IOS: add Starlet LLE milestone through HBC

Boot original Wii firmware through boot0, boot1, boot2 and IOS on an emulated ARM Starlet. Model the required IPC, memory, SD, USB and Bluetooth hardware behavior, including IOS reload into IOS58, and add focused tests, launch utilities and architecture documentation.
This commit is contained in:
2026-08-25 17:11:49 +02:00
parent 38e70fda65
commit a77c156bc8
46 changed files with 10556 additions and 158 deletions
+18
View File
@@ -48,3 +48,21 @@ CMakeLists.txt.user
.cache/clangd
# Ignore CMake user presets
CMakeUserPresets.json
# Private Wii firmware, NAND, and console keys used for local LLE development
/dumps/
# Local Starlet build, test, and isolated user directories
/.starlet_*/
# Local Wii SD contents and generated diagnostics
/Virtual-SD/
/*.png
/*.bin
/*.elf
/letterbomb-log-marker.txt
# Locally downloaded reverse-engineering tools
/.tools/
/capstone_local/
/capstone_runtime/
/capstone-*.whl
+43
View File
@@ -0,0 +1,43 @@
param(
[ValidateSet('Open', 'List', 'RefreshLetterBomb')]
[string]$Action = 'Open',
[string]$MacAddress = '02:00:00:43:18:01',
[ValidateSet('E', 'U', 'J', 'K')]
[string]$Region = 'E'
)
$repoRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$sdFolder = Join-Path $repoRoot 'Virtual-SD'
$generator = Join-Path $repoRoot 'Tools\Generate-LetterBomb.py'
$runningDolphin = Get-Process -Name DolphinNoGUI,Dolphin -ErrorAction SilentlyContinue
if ($runningDolphin -and $Action -ne 'List') {
throw 'Ferme Dolphin avant de modifier Virtual-SD : son arrêt resynchronise le dossier.'
}
if (-not (Test-Path -LiteralPath $sdFolder)) {
New-Item -ItemType Directory -Path $sdFolder | Out-Null
}
switch ($Action) {
'Open' {
Start-Process -FilePath 'explorer.exe' -ArgumentList @($sdFolder)
}
'List' {
Get-ChildItem -LiteralPath $sdFolder -Recurse -Force |
Select-Object FullName, Length, LastWriteTime
}
'RefreshLetterBomb' {
$python = Get-Command python.exe -ErrorAction Stop
& $python.Source $generator `
--mac $MacAddress `
--region $Region `
--output $sdFolder
if ($LASTEXITCODE -ne 0) {
throw "La génération de LetterBomb a échoué ($LASTEXITCODE)."
}
Write-Host 'LetterBomb actualisé. Il sera copié dans WiiSD.raw au prochain lancement.'
}
}
+2
View File
@@ -0,0 +1,2 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Manage-Wii-SD.ps1" Open
+2
View File
@@ -0,0 +1,2 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Run-Wii-IOS-LLE.ps1"
+40
View File
@@ -0,0 +1,40 @@
param(
[string]$BuildDirectory = '.starlet_msvc2',
[string]$UserDirectory = '.starlet_user3',
[switch]$Wait
)
$repoRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$dolphinPath = Join-Path $repoRoot "$BuildDirectory\Binaries\DolphinNoGUI.exe"
$userPath = Join-Path $repoRoot $UserDirectory
if (-not (Test-Path -LiteralPath $dolphinPath)) {
throw "DolphinNoGUI.exe is missing: $dolphinPath"
}
$dolphin = Start-Process -FilePath $dolphinPath -ArgumentList @(
'-u', $userPath,
'-n', '0000000100000002',
'-v', 'D3D',
'-p', 'win32'
) -WorkingDirectory $repoRoot -PassThru
$dolphin.PriorityClass = 'High'
# Windows otherwise tends to schedule the ARM interpreter on an efficient core
# of this hybrid CPU. Logical processors 0-3 are its two performance cores.
$processorName = (Get-ItemPropertyValue `
-LiteralPath 'HKLM:\HARDWARE\DESCRIPTION\System\CentralProcessor\0' `
-Name 'ProcessorNameString' -ErrorAction SilentlyContinue)
if ($processorName -match '12th Gen Intel\(R\) Core\(TM\) i7-1255U') {
$dolphin.ProcessorAffinity = [IntPtr]0xF
}
Write-Host "Dolphin IOS LLE started (PID $($dolphin.Id))."
if ($Wait) {
$dolphin.WaitForExit()
exit $dolphin.ExitCode
}
+36
View File
@@ -534,6 +534,14 @@ bool CBoot::BootUp(Core::System& system, const Core::CPUThreadGuard& guard,
if (!volume)
return false;
if (system.GetStarlet())
{
NOTICE_LOG_FMT(BOOT,
"Wii IOS LLE: disc inserted; the original Starlet boot chain will start "
"Broadway instead of Dolphin's emulated BS2");
return true;
}
if (!EmulatedBS2(system, guard, system.IsWii(), *volume, riivolution_patches))
return false;
@@ -548,6 +556,13 @@ bool CBoot::BootUp(Core::System& system, const Core::CPUThreadGuard& guard,
if (!executable.reader->IsValid())
return false;
if (system.GetStarlet())
{
PanicAlertFmtT("Direct executable boot is unavailable with Wii IOS LLE. Boot the NAND "
"through Starlet, then launch the title through the original IOS.");
return false;
}
SetDefaultDisc(system.GetDVDInterface());
auto& ppc_state = system.GetPPCState();
@@ -611,6 +626,12 @@ bool CBoot::BootUp(Core::System& system, const Core::CPUThreadGuard& guard,
bool operator()(const DiscIO::VolumeWAD& wad) const
{
if (system.GetStarlet())
{
PanicAlertFmtT("Direct WAD boot is unavailable with Wii IOS LLE. Install and launch the "
"title through the original Wii software.");
return false;
}
SetDefaultDisc(system.GetDVDInterface());
if (!Boot_WiiWAD(system, wad))
return false;
@@ -623,6 +644,21 @@ bool CBoot::BootUp(Core::System& system, const Core::CPUThreadGuard& guard,
bool operator()(const BootParameters::NANDTitle& nand_title) const
{
if (system.GetStarlet())
{
constexpr u64 SYSTEM_MENU_TITLE_ID = 0x0000000100000002;
if (nand_title.id != SYSTEM_MENU_TITLE_ID)
{
PanicAlertFmtT("Direct NAND-title boot is unavailable with Wii IOS LLE. Select the Wii "
"System Menu to start the original NAND boot chain.");
return false;
}
SetDefaultDisc(system.GetDVDInterface());
AchievementManager::GetInstance().LoadGame(nullptr);
NOTICE_LOG_FMT(BOOT, "Wii IOS LLE: starting the original NAND boot chain");
return true;
}
SetDefaultDisc(system.GetDVDInterface());
if (!BootNANDTitle(system, nand_title.id))
return false;
+6
View File
@@ -402,6 +402,12 @@ add_library(core
IOS/IOSC.h
IOS/MIOS.cpp
IOS/MIOS.h
IOS/Starlet/ARMCore.cpp
IOS/Starlet/ARMCore.h
IOS/Starlet/Starlet.cpp
IOS/Starlet/Starlet.h
IOS/Starlet/StarletMemory.cpp
IOS/Starlet/StarletMemory.h
IOS/Network/ICMP.h
IOS/Network/ICMPLin.cpp
IOS/Network/IP/Top.cpp
+2
View File
@@ -256,6 +256,7 @@ const Info<u32> MAIN_ARAM_EXPANSION_SIZE{{System::Main, "Core", "ARAMExpansionSi
const Info<std::string> MAIN_GPU_DETERMINISM_MODE{{System::Main, "Core", "GPUDeterminismMode"},
"auto"};
const Info<s32> MAIN_OVERRIDE_BOOT_IOS{{System::Main, "Core", "OverrideBootIOS"}, -1};
const Info<bool> MAIN_WII_IOS_LLE{{System::Main, "Core", "WiiIOSLLE"}, false};
GPUDeterminismMode GetGPUDeterminismMode()
{
@@ -333,6 +334,7 @@ const Info<std::string> MAIN_DUMP_PATH{{System::Main, "General", "DumpPath"}, ""
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_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_SD_CARD_IMAGE_PATH{{System::Main, "General", "WiiSDCardPath"}, ""};
const Info<std::string> MAIN_WII_SD_CARD_SYNC_FOLDER_PATH{
{System::Main, "General", "WiiSDCardSyncFolder"}, ""};
+2
View File
@@ -166,6 +166,7 @@ extern const Info<bool> MAIN_ENABLE_SAVESTATES;
extern const Info<DiscIO::Region> MAIN_FALLBACK_REGION;
extern const Info<bool> MAIN_REAL_WII_REMOTE_REPEAT_REPORTS;
extern const Info<s32> MAIN_OVERRIDE_BOOT_IOS;
extern const Info<bool> MAIN_WII_IOS_LLE;
extern const Info<std::string> MAIN_WII_NUS_SHOP_URL;
extern const Info<bool> MAIN_WII_WIILINK_ENABLE;
@@ -207,6 +208,7 @@ extern const Info<std::string> MAIN_DUMP_PATH;
extern const Info<std::string> MAIN_LOAD_PATH;
extern const Info<std::string> MAIN_RESOURCEPACK_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_SD_CARD_IMAGE_PATH;
extern const Info<std::string> MAIN_WII_SD_CARD_SYNC_FOLDER_PATH;
extern const Info<std::string> MAIN_WFS_PATH;
+20
View File
@@ -427,6 +427,26 @@ struct SetGameMetadata
bool operator()(const BootParameters::NANDTitle& nand_title) const
{
constexpr u64 SYSTEM_MENU_TITLE_ID = 0x0000000100000002;
if (Config::Get(Config::MAIN_WII_IOS_LLE))
{
if (nand_title.id != SYSTEM_MENU_TITLE_ID)
{
PanicAlertFmtT("Direct NAND-title boot is unavailable with Wii IOS LLE. Select the Wii "
"System Menu to start the original NAND boot chain.");
return false;
}
// The LLE NAND remains a raw, read-only dump with a copy-on-write overlay. Do not ask the
// HLE filesystem for a TMD here: boot2 and the original IOS will select and authenticate the
// System Menu themselves.
*region = DiscIO::Region::Unknown;
system.SetIsWii(true);
config->SetRunningGameMetadata("WIILLE");
Host_TitleChanged();
return true;
}
IOS::HLE::Kernel ios;
const IOS::ES::TMDReader tmd = ios.GetESCore().FindInstalledTMD(nand_title.id);
if (!tmd.IsValid() || !IOS::ES::IsChannel(nand_title.id))
+5 -1
View File
@@ -650,10 +650,14 @@ static void EmuThread(Core::System& system, std::unique_ptr<BootParameters> boot
Core::CleanUpWiiFileSystemContents(boot_session_data);
boot_session_data.InvokeWiiSyncCleanup();
}};
if (system.IsWii())
if (system.IsWii() && !system.GetStarlet())
Core::InitializeWiiFileSystemContents(savegame_redirect, boot_session_data);
else
{
// Starlet LLE mounts the raw NAND dump through its hardware NAND controller and maintains its
// own copy-on-write overlay. The HLE Wii filesystem is deliberately absent in this mode.
wiifs_guard.Dismiss();
}
// This adds the SyncGPU handler to CoreTiming, so now CoreTiming::Advance might block.
system.GetFifo().Prepare();
+3
View File
@@ -35,6 +35,7 @@
#include "Core/HW/ProcessorInterface.h"
#include "Core/HW/StreamADPCM.h"
#include "Core/HW/SystemTimers.h"
#include "Core/HW/WII_IPC.h"
#include "Core/IOS/DI/DI.h"
#include "Core/IOS/IOS.h"
#include "Core/Movie.h"
@@ -636,6 +637,8 @@ void DVDInterface::UpdateInterrupts()
(m_DISR.BRKINT & m_DISR.BRKINTMASK) != 0 || (m_DICVR.CVRINT & m_DICVR.CVRINTMASK) != 0;
m_system.GetProcessorInterface().SetInterrupt(ProcessorInterface::INT_CAUSE_DI, set_mask);
if (m_system.GetStarlet())
m_system.GetWiiIPC().SetStarletInterrupt(IOS::INT_CAUSE_DI, set_mask);
// Required for Summoner: A Goddess Reborn
m_system.GetCoreTiming().ForceExceptionCheck(50);
+22 -16
View File
@@ -33,8 +33,9 @@
namespace ExpansionInterface
{
// We should provide an option to choose from the above, or figure out the checksum (the algo in
// yagcd seems wrong) so that people can change default language.
// We should provide an option to choose from the above, or figure out the
// checksum (the algo in yagcd seems wrong) so that people can change default
// language.
static const char iplverPAL[0x100] = "(C) 1999-2001 Nintendo. All rights reserved."
"(C) 1999 ArtX Inc. All rights reserved."
@@ -117,7 +118,8 @@ CEXIIPL::CEXIIPL(Core::System& system) : IEXIDevice(system)
}
else
{
// If we are in Wii mode or if loading the GC IPL fails, we should still try to load fonts.
// If we are in Wii mode or if loading the GC IPL fails, we should still try
// to load fonts.
// Copy header
if (DiscIO::IsNTSC(SConfig::GetInstance().m_region))
@@ -135,8 +137,8 @@ CEXIIPL::CEXIIPL(Core::System& system) : IEXIDevice(system)
// Clear RTC
sram.rtc = 0;
// We Overwrite language selection here since it's possible on the GC to change the language as
// you please
// We Overwrite language selection here since it's possible on the GC to
// change the language as you please
sram.settings.language = Config::Get(Config::MAIN_GC_LANGUAGE);
sram.settings.rtc_bias = 0;
FixSRAMChecksums(&sram);
@@ -201,10 +203,11 @@ bool CEXIIPL::HasIPLDump()
void CEXIIPL::LoadFontFile(const std::string& filename, u32 offset)
{
// Official IPL fonts are copyrighted. Dolphin ships with a set of free font alternatives but
// unfortunately the bundled fonts have different padding, causing issues with misplaced text
// in some titles. This function check if the user has IPL dumps available and load the fonts
// from those dumps instead of loading the bundled fonts
// Official IPL fonts are copyrighted. Dolphin ships with a set of free font
// alternatives but unfortunately the bundled fonts have different padding,
// causing issues with misplaced text in some titles. This function check if
// the user has IPL dumps available and load the fonts from those dumps
// instead of loading the bundled fonts
if (!Config::Get(Config::SESSION_LOAD_IPL_DUMP))
{
@@ -229,8 +232,9 @@ void CEXIIPL::LoadFontFile(const std::string& filename, u32 offset)
return;
}
// Official Windows-1252 and Shift JIS fonts present on the IPL dumps are 0x2575 and 0x4a24d
// bytes long respectively, so, determine the size of the font being loaded based on the offset
// Official Windows-1252 and Shift JIS fonts present on the IPL dumps are
// 0x2575 and 0x4a24d bytes long respectively, so, determine the size of the
// font being loaded based on the offset
const u64 fontsize = (offset == 0x1aff00) ? 0x4a24d : 0x2575;
INFO_LOG_FMT(BOOT, "Found IPL dump, loading {} font from {}",
@@ -321,21 +325,23 @@ void CEXIIPL::TransferByte(u8& data)
u32 dev_addr = address - ROM_BASE + m_cursor++;
// TODO: Is this address wrapping correct? Needs a hardware test
dev_addr %= ROM_SIZE;
// Technically we should descramble here iff descrambling logic is enabled.
// At the moment, we pre-decrypt the whole thing and
// ignore the "enabled" bit - see CEXIIPL::CEXIIPL
// Technically we should descramble here iff descrambling logic is
// enabled. At the moment, we pre-decrypt the whole thing and ignore the
// "enabled" bit - see CEXIIPL::CEXIIPL
data = m_rom[dev_addr];
if ((dev_addr >= 0x001AFF00) && (dev_addr <= 0x001FF474) && !m_fonts_loaded)
{
if (dev_addr >= 0x001FCF00)
{
PanicAlertFmtT("Error: Trying to access Windows-1252 fonts but they are not loaded. "
PanicAlertFmtT("Error: Trying to access Windows-1252 fonts but "
"they are not loaded. "
"Games may not show fonts correctly, or crash.");
}
else
{
PanicAlertFmtT("Error: Trying to access Shift JIS fonts but they are not loaded. "
PanicAlertFmtT("Error: Trying to access Shift JIS fonts but they "
"are not loaded. "
"Games may not show fonts correctly, or crash.");
}
// Don't be a nag
+38
View File
@@ -3,7 +3,11 @@
#include "Core/HW/HW.h"
#include <memory>
#include <string>
#include "Common/ChunkFile.h"
#include "Common/MsgHandler.h"
#include "Core/Config/MainSettings.h"
#include "Core/Core.h"
@@ -24,6 +28,7 @@
#include "Core/HW/VideoInterface.h"
#include "Core/HW/WII_IPC.h"
#include "Core/IOS/IOS.h"
#include "Core/IOS/Starlet/Starlet.h"
#include "Core/State.h"
#include "Core/System.h"
@@ -55,8 +60,27 @@ void Init(Core::System& system, const Sram* override_sram)
if (system.IsWii())
{
system.GetWiiIPC().Init();
if (Config::Get(Config::MAIN_WII_IOS_LLE))
{
auto starlet = std::make_unique<IOS::LLE::Starlet>(system);
std::string error;
if (starlet->Init(Config::Get(Config::MAIN_WII_LLE_DUMP_PATH), &error))
{
system.SetStarlet(std::move(starlet));
system.GetWiiIPC().HoldBroadwayForStarletBoot();
}
else
{
PanicAlertFmtT("Could not start Wii IOS LLE: {0}", error);
Core::SetState(system, Core::State::Stopping);
return;
}
}
else
{
IOS::HLE::Init(system); // Depends on Memory
}
}
system.GetMemory().InitMMIO(system);
}
@@ -64,6 +88,12 @@ void Init(Core::System& system, const Sram* override_sram)
void Shutdown(Core::System& system)
{
// IOS should always be shut down regardless of IsWii because it can be running in GC mode (MIOS).
if (system.GetStarlet())
{
system.GetStarlet()->Shutdown();
system.SetStarlet(nullptr);
}
if (system.GetIOS())
IOS::HLE::Shutdown(system); // Depends on Memory
system.GetWiiIPC().Shutdown();
@@ -112,9 +142,17 @@ void DoState(Core::System& system, PointerWrap& p)
{
system.GetWiiIPC().DoState(p);
p.DoMarker("IOS");
if (system.GetStarlet())
{
system.GetStarlet()->DoState(p);
p.DoMarker("IOS::LLE");
}
else if (system.GetIOS())
{
system.GetIOS()->DoState(p);
p.DoMarker("IOS::HLE");
}
}
p.DoMarker("WIIHW");
}
+1
View File
@@ -665,6 +665,7 @@ void MemoryManager::CopyToEmu(u32 address, const void* data, size_t size)
PanicAlertFmt("Invalid range in CopyToEmu. {:x} bytes to {:#010x}", size, address);
return;
}
memcpy(pointer, data, size);
}
+1 -1
View File
@@ -95,7 +95,7 @@ void SystemTimersManager::AudioDMACallback(Core::System& system, u64 userdata, s
void SystemTimersManager::IPC_HLE_UpdateCallback(Core::System& system, u64 userdata,
s64 cycles_late)
{
if (system.IsWii())
if (system.IsWii() && system.GetIOS())
{
system.GetIOS()->UpdateDevices();
auto& system_timers = system.GetSystemTimers();
+240 -35
View File
@@ -12,17 +12,19 @@
#include "Core/HW/MMIO.h"
#include "Core/HW/ProcessorInterface.h"
#include "Core/IOS/IOS.h"
#include "Core/IOS/Starlet/Starlet.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/System.h"
// This is the intercommunication between ARM and PPC. Currently only PPC actually uses it, because
// of the IOS HLE
// How IOS uses IPC:
// X1 Execute command: a new pointer is available in HW_IPC_PPCCTRL
// X2 Reload (a new IOS is being loaded, old one doesn't need to reply anymore)
// Y1 Command executed and reply available in HW_IPC_ARMMSG
// Y2 Command acknowledge
// m_ppc_msg is a pointer to 0x40byte command structure
// m_arm_msg is, similarly, starlet's response buffer*
// This is the intercommunication between ARM and PPC. Producer flags remain
// asserted until the peer acknowledges them; WII_IPC.h owns that register
// behavior. Currently only PPC
// actually uses it, because of the IOS HLE How IOS uses IPC: X1 Execute
// command: a new pointer is available in HW_IPC_PPCCTRL X2 Reload (a new IOS is
// being loaded, old one doesn't need to reply anymore) Y1 Command executed and
// reply available in HW_IPC_ARMMSG Y2 Command acknowledge m_ppc_msg is a
// pointer to 0x40byte command structure m_arm_msg is, similarly, starlet's
// response buffer*
namespace IOS
{
@@ -40,6 +42,7 @@ enum
PPC_IRQMASK = 0x34,
ARM_IRQFLAG = 0x38,
ARM_IRQMASK = 0x3c,
ARM_FIQMASK = 0x40,
AHBPROT = 0x64,
@@ -52,6 +55,8 @@ enum
GPIO_IN = 0xe8,
HW_RESETS = 0x194,
OTP_CMD = 0x1ec,
OTP_DATA = 0x1f0,
UNK_180 = 0x180,
UNK_1CC = 0x1cc,
@@ -61,6 +66,10 @@ enum
// Indicates which pins are accessible by broadway. Writable by starlet only.
static constexpr Common::Flags<GPIO> gpio_owner = {GPIO::SLOT_LED, GPIO::SLOT_IN, GPIO::SENSOR_BAR,
GPIO::DO_EJECT, GPIO::AVE_SCL, GPIO::AVE_SDA};
constexpr u32 PPC_RESET_MASK = 0x30;
// boot2 leaves all AHB permissions enabled. IOS syscall 54 then clears or restores the
// Broadway permission bits according to the launched title's TMD access-rights field.
u32 s_ahbprot = 0xffffffff;
WiiIPC::WiiIPC(Core::System& system) : m_system(system)
{
@@ -77,9 +86,12 @@ void WiiIPC::DoState(PointerWrap& p)
p.Do(m_ppc_irq_masks);
p.Do(m_arm_irq_flags);
p.Do(m_arm_irq_masks);
p.Do(m_arm_fiq_masks);
p.Do(s_ahbprot);
p.Do(m_gpio_dir);
p.Do(m_gpio_out);
p.Do(m_resets);
p.Do(m_broadway_in_reset);
}
void WiiIPC::InitState()
@@ -92,9 +104,11 @@ void WiiIPC::InitState()
m_ppc_irq_masks = 0;
m_arm_irq_flags = 0;
m_arm_irq_masks = 0;
m_arm_fiq_masks = 0;
s_ahbprot = 0xffffffff;
// The only inputs are POWER, EJECT_BTN, SLOT_IN, and EEP_MISO; Broadway only has access to
// SLOT_IN
// The only inputs are POWER, EJECT_BTN, SLOT_IN, and EEP_MISO; Broadway only
// has access to SLOT_IN
m_gpio_dir = {
GPIO::POWER, GPIO::SHUTDOWN, GPIO::FAN, GPIO::DC_DC, GPIO::DI_SPIN, GPIO::SLOT_LED,
GPIO::SENSOR_BAR, GPIO::DO_EJECT, GPIO::EEP_CS, GPIO::EEP_CLK, GPIO::EEP_MOSI, GPIO::AVE_SCL,
@@ -103,9 +117,10 @@ void WiiIPC::InitState()
};
m_gpio_out = {};
// A cleared bit indicates the device is reset/off, so set everything to 1 (this may not exactly
// match hardware)
// A cleared bit indicates the device is reset/off, so set everything to 1
// (this may not exactly match hardware)
m_resets = 0xffffffff;
m_broadway_in_reset = false;
m_ppc_irq_masks |= INT_CAUSE_IPC_BROADWAY;
}
@@ -129,7 +144,16 @@ void WiiIPC::Shutdown()
void WiiIPC::RegisterMMIO(MMIO::Mapping* mmio, u32 base)
{
mmio->Register(base | IPC_PPCMSG, MMIO::InvalidRead<u32>(), MMIO::DirectWrite<u32>(&m_ppc_msg));
// Both IPC message registers are general-purpose read/write registers on
// Hollywood. IOS normally uses PPCMSG in the Broadway-to-Starlet direction
// and ARMMSG in the other direction, but MINI uses both as paired 16-bit
// circular-queue indices and performs read/modify/write accesses from
// Broadway.
mmio->Register(base | IPC_PPCMSG, MMIO::DirectRead<u32>(&m_ppc_msg),
MMIO::ComplexWrite<u32>([](Core::System& system, u32, u32 val) {
auto& wii_ipc = system.GetWiiIPC();
wii_ipc.m_ppc_msg = val;
}));
mmio->Register(base | IPC_PPCCTRL, MMIO::ComplexRead<u32>([](Core::System& system, u32) {
auto& wii_ipc = system.GetWiiIPC();
@@ -143,36 +167,47 @@ void WiiIPC::RegisterMMIO(MMIO::Mapping* mmio, u32 base)
if ((val >> 2 & 1 && wii_ipc.m_ctrl.IY1) || (val >> 1 & 1 && wii_ipc.m_ctrl.IY2))
wii_ipc.m_ppc_irq_flags |= INT_CAUSE_IPC_BROADWAY;
if (wii_ipc.m_ctrl.X1)
{
if (system.GetStarlet())
wii_ipc.m_arm_irq_flags |= INT_CAUSE_IPC_STARLET;
else if (system.GetIOS())
system.GetIOS()->EnqueueIPCRequest(wii_ipc.m_ppc_msg);
}
if (system.GetIOS())
system.GetIOS()->UpdateIPC();
system.GetCoreTiming().ScheduleEvent(0, wii_ipc.m_event_type_update_interrupts,
0);
}));
mmio->Register(base | IPC_ARMMSG, MMIO::DirectRead<u32>(&m_arm_msg), MMIO::InvalidWrite<u32>());
mmio->Register(base | IPC_ARMMSG, MMIO::DirectRead<u32>(&m_arm_msg),
MMIO::DirectWrite<u32>(&m_arm_msg));
mmio->Register(base | PPC_IRQFLAG, MMIO::InvalidRead<u32>(),
mmio->Register(base | PPC_IRQFLAG, MMIO::DirectRead<u32>(&m_ppc_irq_flags),
MMIO::ComplexWrite<u32>([](Core::System& system, u32, u32 val) {
auto& wii_ipc = system.GetWiiIPC();
wii_ipc.m_ppc_irq_flags &= ~val;
if (system.GetIOS())
system.GetIOS()->UpdateIPC();
system.GetCoreTiming().ScheduleEvent(0, wii_ipc.m_event_type_update_interrupts,
0);
}));
mmio->Register(base | PPC_IRQMASK, MMIO::InvalidRead<u32>(),
mmio->Register(base | PPC_IRQMASK, MMIO::DirectRead<u32>(&m_ppc_irq_masks),
MMIO::ComplexWrite<u32>([](Core::System& system, u32, u32 val) {
auto& wii_ipc = system.GetWiiIPC();
wii_ipc.m_ppc_irq_masks = val;
if (wii_ipc.m_ppc_irq_masks & INT_CAUSE_IPC_BROADWAY) // wtf?
if (!system.GetStarlet() &&
(wii_ipc.m_ppc_irq_masks & INT_CAUSE_IPC_BROADWAY)) // HLE handshake
wii_ipc.Reset();
if (system.GetIOS())
system.GetIOS()->UpdateIPC();
system.GetCoreTiming().ScheduleEvent(0, wii_ipc.m_event_type_update_interrupts,
0);
}));
// Dolphin currently does not emulate any hardware access restrictions.
mmio->Register(base | AHBPROT, MMIO::Constant<u32>(0xFFFFFFFF), MMIO::InvalidWrite<u32>());
// Access restrictions are not enforced yet, but software still needs to observe the mask that
// the original IOS kernel selected for the running title.
mmio->Register(base | AHBPROT, MMIO::DirectRead<u32>(&s_ahbprot), MMIO::InvalidWrite<u32>());
mmio->Register(base | GPIOB_OUT, MMIO::DirectRead<u32>(&m_gpio_out.m_hex),
MMIO::ComplexWrite<u32>([](Core::System& system, u32, u32 val) {
@@ -200,17 +235,19 @@ void WiiIPC::RegisterMMIO(MMIO::Mapping* mmio, u32 base)
return gpio_in.m_hex;
}),
MMIO::Nop<u32>());
// Starlet GPIO registers, not normally accessible by PPC (but they can be depending on how
// AHBPROT is set up). We just always allow access, since some homebrew uses them.
// Starlet GPIO registers, not normally accessible by PPC (but they can be
// depending on how AHBPROT is set up). We just always allow access, since
// some homebrew uses them.
// Note from WiiBrew: When switching owners, copying of the data is not necessary. For example, if
// pin 0 has certain configuration in the HW_GPIO registers, and that bit is then set in the
// HW_GPIO_OWNER register, those settings will immediately be visible in the HW_GPIOB registers.
// There is only one set of data registers, and the HW_GPIO_OWNER register just controls the
// access that the HW_GPIOB registers have to that data.
// Also: The HW_GPIO registers always have read access to all pins, but any writes (changes) must
// go through the HW_GPIOB registers if the corresponding bit is set in the HW_GPIO_OWNER
// register.
// Note from WiiBrew: When switching owners, copying of the data is not
// necessary. For example, if pin 0 has certain configuration in the HW_GPIO
// registers, and that bit is then set in the HW_GPIO_OWNER register, those
// settings will immediately be visible in the HW_GPIOB registers. There is
// only one set of data registers, and the HW_GPIO_OWNER register just
// controls the access that the HW_GPIOB registers have to that data. Also:
// The HW_GPIO registers always have read access to all pins, but any writes
// (changes) must go through the HW_GPIOB registers if the corresponding bit
// is set in the HW_GPIO_OWNER register.
mmio->Register(base | GPIO_OUT, MMIO::DirectRead<u32>(&m_gpio_out.m_hex),
MMIO::ComplexWrite<u32>([](Core::System& system, u32, u32 val) {
auto& wii_ipc = system.GetWiiIPC();
@@ -253,6 +290,22 @@ void WiiIPC::RegisterMMIO(MMIO::Mapping* mmio, u32 base)
}
}));
// The OTP controller is one physical Hollywood device shared by Broadway and Starlet. Route
// Broadway accesses through the Starlet bus so commands and data use the same keys-backed state.
mmio->Register(base | OTP_CMD, MMIO::ComplexRead<u32>([](Core::System& system, u32 address) {
IOS::LLE::Starlet* const starlet = system.GetStarlet();
return starlet ? starlet->ReadHollywoodRegister(address) : 0;
}),
MMIO::ComplexWrite<u32>([](Core::System& system, u32 address, u32 value) {
if (IOS::LLE::Starlet* const starlet = system.GetStarlet())
starlet->WriteHollywoodRegister(address, value);
}));
mmio->Register(base | OTP_DATA, MMIO::ComplexRead<u32>([](Core::System& system, u32 address) {
IOS::LLE::Starlet* const starlet = system.GetStarlet();
return starlet ? starlet->ReadHollywoodRegister(address) : 0;
}),
MMIO::InvalidWrite<u32>());
// Register some stubbed/unknown MMIOs required to make Wii games work.
mmio->Register(base | PPCSPEED, MMIO::InvalidRead<u32>(), MMIO::Nop<u32>());
mmio->Register(base | VISOLID, MMIO::InvalidRead<u32>(), MMIO::Nop<u32>());
@@ -275,14 +328,166 @@ void WiiIPC::UpdateInterrupts()
if ((m_ctrl.X1 & m_ctrl.IX1) || (m_ctrl.X2 & m_ctrl.IX2))
{
if (m_system.GetStarlet())
m_arm_irq_flags |= INT_CAUSE_IPC_STARLET;
else
m_ppc_irq_flags |= INT_CAUSE_IPC_STARLET;
}
// Generate interrupt on PI if any of the devices behind starlet have an interrupt and mask is set
// Generate interrupt on PI if any of the devices behind starlet have an
// interrupt and mask is set
m_system.GetProcessorInterface().SetInterrupt(ProcessorInterface::INT_CAUSE_WII_IPC,
!!(m_ppc_irq_flags & m_ppc_irq_masks));
}
u32 WiiIPC::ReadStarletRegister(u32 offset) const
{
switch (offset)
{
case IPC_PPCMSG:
return m_ppc_msg;
case IPC_PPCCTRL:
return m_ctrl.ppc();
case IPC_ARMMSG:
return m_arm_msg;
case IPC_ARMCTRL:
return m_ctrl.arm();
case PPC_IRQFLAG:
return m_ppc_irq_flags;
case PPC_IRQMASK:
return m_ppc_irq_masks;
case ARM_IRQFLAG:
return m_arm_irq_flags;
case ARM_IRQMASK:
return m_arm_irq_masks;
case ARM_FIQMASK:
return m_arm_fiq_masks;
case AHBPROT:
return s_ahbprot;
case HW_RESETS:
return m_resets;
default:
return 0;
}
}
void WiiIPC::WriteStarletRegister(u32 offset, u32 value)
{
switch (offset)
{
case IPC_PPCMSG:
m_ppc_msg = value;
break;
case IPC_PPCCTRL:
m_ctrl.ppc(value);
break;
case IPC_ARMMSG:
m_arm_msg = value;
break;
case IPC_ARMCTRL:
m_ctrl.arm(value);
break;
case PPC_IRQFLAG:
m_ppc_irq_flags &= ~value;
break;
case PPC_IRQMASK:
m_ppc_irq_masks = value;
break;
case ARM_IRQFLAG:
m_arm_irq_flags &= ~value;
break;
case ARM_IRQMASK:
m_arm_irq_masks = value;
break;
case ARM_FIQMASK:
m_arm_fiq_masks = value;
break;
case AHBPROT:
DEBUG_LOG_FMT(WII_IPC, "Starlet AHBPROT write: {:#010x} -> {:#010x}", s_ahbprot, value);
s_ahbprot = value;
break;
case HW_RESETS:
{
const u32 old_resets = m_resets;
m_resets = value;
DEBUG_LOG_FMT(WII_IPC, "Starlet HW_RESETS write: {:#010x} -> {:#010x}", old_resets, value);
UpdateBroadwayReset(old_resets, value);
break;
}
default:
return;
}
UpdateInterrupts();
}
void WiiIPC::HoldBroadwayForStarletBoot()
{
auto& power_pc = m_system.GetPowerPC();
// HRESET stops Broadway, but Dolphin's timing loop must keep running so
// Starlet can advance. MMU::TryReadInstruction supplies a synthetic branch at
// BROADWAY_HOLD_PC while this flag is set, which keeps the timing loop alive
// without corrupting MEM1.
m_resets &= ~PPC_RESET_MASK;
m_broadway_in_reset = true;
power_pc.Reset();
power_pc.GetPPCState().pc = BROADWAY_HOLD_PC;
power_pc.GetPPCState().npc = BROADWAY_HOLD_PC;
}
void WiiIPC::UpdateBroadwayReset(u32 old_resets, u32 new_resets)
{
// IOS releases SRESET first and HRESET second. Broadway is runnable only
// after both are high.
const bool was_released = (old_resets & PPC_RESET_MASK) == PPC_RESET_MASK;
const bool is_released = (new_resets & PPC_RESET_MASK) == PPC_RESET_MASK;
if (was_released && !is_released)
{
HoldBroadwayForStarletBoot();
}
else if (!was_released && is_released)
{
const auto* starlet = m_system.GetStarlet();
if (!starlet ||
!starlet->TryReadBroadwayResetInstruction(BROADWAY_RESET_VECTOR_BASE).has_value())
{
m_resets &= ~PPC_RESET_MASK;
WARN_LOG_FMT(WII_IPC, "Starlet released Broadway before enabling the EXI "
"boot vector; keeping "
"Broadway in HRESET");
return;
}
auto& power_pc = m_system.GetPowerPC();
power_pc.Reset();
auto& state = power_pc.GetPPCState();
state.msr.IP = 1;
state.pc = BROADWAY_RESET_VECTOR_BASE;
state.npc = BROADWAY_RESET_VECTOR_BASE + 4;
m_broadway_in_reset = false;
INFO_LOG_FMT(WII_IPC, "Starlet released Broadway through the EXI boot vector");
}
}
bool WiiIPC::IsStarletIRQAsserted() const
{
return (m_arm_irq_flags & m_arm_irq_masks) != 0;
}
bool WiiIPC::IsStarletFIQAsserted() const
{
return (m_arm_irq_flags & m_arm_fiq_masks) != 0;
}
void WiiIPC::SetStarletInterrupt(u32 cause, bool asserted)
{
if (asserted)
m_arm_irq_flags |= cause;
else
m_arm_irq_flags &= ~cause;
UpdateInterrupts();
}
void WiiIPC::ClearX1()
{
m_ctrl.X1 = 0;
@@ -293,8 +498,8 @@ void WiiIPC::GenerateAck(u32 address)
m_ctrl.Y2 = 1;
DEBUG_LOG_FMT(WII_IPC, "GenerateAck: {:08x} | {:08x} [R:{} A:{} E:{}]", m_ppc_msg, address,
m_ctrl.Y1, m_ctrl.Y2, m_ctrl.X1);
// Based on a hardware test, the IPC interrupt takes approximately 100 TB ticks to fire
// after Y2 is seen in the control register.
// Based on a hardware test, the IPC interrupt takes approximately 100 TB
// ticks to fire after Y2 is seen in the control register.
m_system.GetCoreTiming().ScheduleEvent(100_tbticks, m_event_type_update_interrupts);
}
@@ -304,8 +509,8 @@ void WiiIPC::GenerateReply(u32 address)
m_ctrl.Y1 = 1;
DEBUG_LOG_FMT(WII_IPC, "GenerateReply: {:08x} | {:08x} [R:{} A:{} E:{}]", m_ppc_msg, address,
m_ctrl.Y1, m_ctrl.Y2, m_ctrl.X1);
// Based on a hardware test, the IPC interrupt takes approximately 100 TB ticks to fire
// after Y1 is seen in the control register.
// Based on a hardware test, the IPC interrupt takes approximately 100 TB
// ticks to fire after Y1 is seen in the control register.
m_system.GetCoreTiming().ScheduleEvent(100_tbticks, m_event_type_update_interrupts);
}
+31 -6
View File
@@ -33,6 +33,7 @@ enum StarletInterruptCause
INT_CAUSE_OHCI1 = 0x40,
INT_CAUSE_SD = 0x80,
INT_CAUSE_WIFI = 0x100,
INT_CAUSE_DI = 0x200,
INT_CAUSE_GPIO_BROADWAY = 0x400,
INT_CAUSE_GPIO_STARLET = 0x800,
@@ -83,12 +84,19 @@ struct CtrlRegister
u8 IY2 : 1;
CtrlRegister() { X1 = X2 = Y1 = Y2 = IX1 = IX2 = IY1 = IY2 = 0; }
inline u8 ppc() { return (IY2 << 5) | (IY1 << 4) | (X2 << 3) | (Y1 << 2) | (Y2 << 1) | X1; }
inline u8 arm() { return (IX2 << 5) | (IX1 << 4) | (Y2 << 3) | (X1 << 2) | (X2 << 1) | Y1; }
inline u8 ppc() const { return (IY2 << 5) | (IY1 << 4) | (X2 << 3) | (Y1 << 2) | (Y2 << 1) | X1; }
inline u8 arm() const { return (IX2 << 5) | (IX1 << 4) | (Y2 << 3) | (X1 << 2) | (X2 << 1) | Y1; }
inline void ppc(u32 v)
{
X1 = v & 1;
X2 = (v >> 3) & 1;
// Broadway owns the X producer flags: writing 1 raises a flag, while
// writing 0 leaves it unchanged. Only Starlet's acknowledgement below is
// allowed to clear it. This preserves a short request until IOS observes
// it, as the two physical CPUs do not execute in one atomic instruction
// stream.
if (v & 1)
X1 = 1;
if ((v >> 3) & 1)
X2 = 1;
if ((v >> 2) & 1)
Y1 = 0;
if ((v >> 1) & 1)
@@ -99,8 +107,11 @@ struct CtrlRegister
inline void arm(u32 v)
{
Y1 = v & 1;
Y2 = (v >> 3) & 1;
// Starlet owns Y1/Y2 with the reciprocal set/acknowledge semantics.
if (v & 1)
Y1 = 1;
if ((v >> 3) & 1)
Y2 = 1;
if ((v >> 2) & 1)
X1 = 0;
if ((v >> 1) & 1)
@@ -113,6 +124,9 @@ struct CtrlRegister
class WiiIPC
{
public:
static constexpr u32 BROADWAY_RESET_VECTOR_BASE = 0xfff00100;
static constexpr u32 BROADWAY_HOLD_PC = 0xfffffffc;
explicit WiiIPC(Core::System& system);
WiiIPC(const WiiIPC&) = delete;
WiiIPC(WiiIPC&&) = delete;
@@ -133,6 +147,14 @@ public:
bool IsReady() const;
u32 ReadStarletRegister(u32 offset) const;
void WriteStarletRegister(u32 offset, u32 value);
void HoldBroadwayForStarletBoot();
bool IsBroadwayInReset() const { return m_broadway_in_reset; }
bool IsStarletIRQAsserted() const;
bool IsStarletFIQAsserted() const;
void SetStarletInterrupt(u32 cause, bool asserted);
Common::Flags<GPIO> GetGPIOOutFlags() const { return m_gpio_out; }
private:
@@ -140,6 +162,7 @@ private:
static void UpdateInterruptsCallback(Core::System& system, u64 userdata, s64 cycles_late);
void UpdateInterrupts();
void UpdateBroadwayReset(u32 old_resets, u32 new_resets);
u32 m_ppc_msg = 0;
u32 m_arm_msg = 0;
@@ -149,11 +172,13 @@ private:
u32 m_ppc_irq_masks = 0;
u32 m_arm_irq_flags = 0;
u32 m_arm_irq_masks = 0;
u32 m_arm_fiq_masks = 0;
Common::Flags<GPIO> m_gpio_dir{};
Common::Flags<GPIO> m_gpio_out{};
u32 m_resets = 0;
bool m_broadway_in_reset = false;
CoreTiming::EventType* m_event_type_update_interrupts = nullptr;
+7
View File
@@ -15,6 +15,7 @@
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
#include "Core/HW/WiimoteReal/WiimoteReal.h"
#include "Core/IOS/IOS.h"
#include "Core/IOS/Starlet/Starlet.h"
#include "Core/IOS/USB/Bluetooth/BTEmu.h"
#include "Core/IOS/USB/Bluetooth/WiimoteDevice.h"
#include "Core/Movie.h"
@@ -65,6 +66,12 @@ namespace WiimoteCommon
{
void UpdateSource(unsigned int index)
{
if (auto* starlet = Core::System::GetInstance().GetStarlet())
{
starlet->SetWiimoteSource(index, GetHIDWiimoteSource(index));
return;
}
const auto bluetooth = WiiUtils::GetBluetoothEmuDevice();
if (bluetooth == nullptr)
return;
File diff suppressed because it is too large Load Diff
+260
View File
@@ -0,0 +1,260 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <cstddef>
#include "Common/CommonTypes.h"
class PointerWrap;
namespace IOS::LLE
{
// Byte-addressed bus used by the Starlet ARM core. Keeping endianness in the
// CPU is intentional: ARM926 can change its data endianness through CP15 while
// the underlying devices remain byte addressed.
class ARMBus
{
public:
virtual ~ARMBus() = default;
virtual u8 Read8(u32 address) = 0;
virtual u16 Read16(u32 address)
{
return static_cast<u16>((static_cast<u16>(Read8(address)) << 8) | Read8(address + 1));
}
virtual u32 Read32(u32 address)
{
return (static_cast<u32>(Read8(address)) << 24) | (static_cast<u32>(Read8(address + 1)) << 16) |
(static_cast<u32>(Read8(address + 2)) << 8) | Read8(address + 3);
}
virtual void Write8(u32 address, u8 value) = 0;
virtual void Write16(u32 address, u16 value)
{
Write8(address, static_cast<u8>(value >> 8));
Write8(address + 1, static_cast<u8>(value));
}
virtual void Write32(u32 address, u32 value)
{
Write8(address, static_cast<u8>(value >> 24));
Write8(address + 1, static_cast<u8>(value >> 16));
Write8(address + 2, static_cast<u8>(value >> 8));
Write8(address + 3, static_cast<u8>(value));
}
virtual void AdvanceCycles(u64 cycles) {}
virtual bool IsIdlePollAddress(u32 address, u32 size) const { return false; }
};
class ARMCore final
{
public:
enum class Mode : u32
{
User = 0x10,
FIQ = 0x11,
IRQ = 0x12,
Supervisor = 0x13,
Abort = 0x17,
Undefined = 0x1b,
System = 0x1f,
};
struct CP15State
{
u32 control = 0;
u32 translation_table_base = 0;
u32 domain_access_control = 0;
u32 fault_status = 0;
u32 fault_address = 0;
u32 process_id = 0;
};
static constexpr u32 CPSR_N = 1U << 31;
static constexpr u32 CPSR_Z = 1U << 30;
static constexpr u32 CPSR_C = 1U << 29;
static constexpr u32 CPSR_V = 1U << 28;
static constexpr u32 CPSR_Q = 1U << 27;
static constexpr u32 CPSR_I = 1U << 7;
static constexpr u32 CPSR_F = 1U << 6;
static constexpr u32 CPSR_T = 1U << 5;
static constexpr u32 CPSR_MODE_MASK = 0x1f;
explicit ARMCore(ARMBus& bus);
void Reset(u32 reset_vector = 0);
int Step();
u64 Run(u64 instruction_count);
u64 RunCycles(u64 cycle_budget);
void DoState(::PointerWrap& p);
u32 GetRegister(size_t index) const;
void SetRegister(size_t index, u32 value);
u32 GetCPSR() const { return m_cpsr; }
void SetCPSR(u32 value);
Mode GetMode() const;
const CP15State& GetCP15State() const { return m_cp15; }
CP15State& GetCP15State() { return m_cp15; }
void SetIRQLine(bool asserted)
{
m_irq_line = asserted;
if (asserted)
{
m_waiting_for_interrupt = false;
if ((m_cpsr & CPSR_I) == 0)
m_waiting_for_memory_poll = false;
}
}
void SetFIQLine(bool asserted)
{
m_fiq_line = asserted;
if (asserted)
{
m_waiting_for_interrupt = false;
if ((m_cpsr & CPSR_F) == 0)
m_waiting_for_memory_poll = false;
}
}
bool IsWaitingForInterrupt() const { return m_waiting_for_interrupt; }
bool IsWaitingForMemoryPoll() const { return m_waiting_for_memory_poll; }
u32 GetMemoryPollAddress() const { return m_memory_poll_address; }
bool IsWaitingForExternalEvent() const
{
return m_waiting_for_interrupt || m_waiting_for_memory_poll;
}
void RequestYield() { m_yield_requested = true; }
bool IsBigEndian() const { return m_big_endian; }
void SetBigEndian(bool big_endian);
u32 GetLastUndefinedInstruction() const { return m_last_undefined_instruction; }
u64 GetExecutedInstructions() const { return m_executed_instructions; }
private:
struct ShiftResult
{
u32 value;
bool carry;
};
struct AddResult
{
u32 value;
bool carry;
bool overflow;
};
struct TLBEntry
{
u32 virtual_page = 0;
u32 physical_page = 0;
u32 generation = 0;
};
struct InstructionCacheEntry
{
u32 physical_word = 0;
u32 raw_value = 0;
u32 generation = 0;
};
static constexpr size_t TLB_ENTRY_COUNT = 4096;
static constexpr size_t INSTRUCTION_CACHE_ENTRY_COUNT = 16384;
static bool IsValidMode(u32 mode);
static s32 SignExtend(u32 value, unsigned bits);
static AddResult AddWithCarry(u32 lhs, u32 rhs, bool carry);
int StepInternal(bool advance_bus);
bool HasUnmaskedInterrupt() const;
bool TryEnterThumbMemoryPoll(u16 branch_instruction);
u16 Read16(u32 address) const;
u32 Read32(u32 address) const;
u8 ReadByte(u32 address) const;
void Write16(u32 address, u16 value);
void Write32(u32 address, u32 value);
void WriteByte(u32 address, u8 value);
u32 TranslateVirtualAddress(u32 address) const;
u32 ReadPhysical32(u32 address) const;
void InvalidateTLB();
u16 FetchThumbInstruction(u32 address);
u32 FetchARMInstruction(u32 address);
void InvalidateInstructionCache();
u32 ReadRegisterOperand(u32 index) const;
u32 ReadRegisterForStore(u32 index) const;
void WritePC(u32 value, bool exchange = false);
void SetNZ(u32 value);
bool ConditionPassed(u32 condition) const;
ShiftResult ShiftImmediate(u32 value, u32 type, u32 amount, bool old_carry) const;
ShiftResult ShiftRegister(u32 value, u32 type, u32 amount, bool old_carry) const;
ShiftResult DecodeOperand2(u32 instruction) const;
void ExecuteARM(u32 instruction);
void ExecuteThumb(u16 instruction);
void ExecuteDataProcessing(u32 instruction);
void ExecuteSingleDataTransfer(u32 instruction);
void ExecuteHalfwordDataTransfer(u32 instruction);
void ExecuteBlockDataTransfer(u32 instruction);
void ExecuteMultiply(u32 instruction);
void ExecuteMultiplyLong(u32 instruction);
void ExecuteBranch(u32 instruction);
void ExecuteCoprocessor(u32 instruction);
void ExecutePSRTransfer(u32 instruction);
void EnterException(Mode mode, u32 vector, u32 return_address, bool disable_fiq = false);
void UndefinedInstruction(u32 instruction);
void SwitchMode(Mode mode);
void SaveBankedRegisters(Mode mode);
void LoadBankedRegisters(Mode mode);
u32* GetSPSR(Mode mode);
const u32* GetSPSR(Mode mode) const;
void RestoreCPSRFromSPSR();
void WritePSR(bool spsr, u32 field_mask, u32 value);
u32 ReadCP15(u32 opcode1, u32 crn, u32 crm, u32 opcode2) const;
void WriteCP15(u32 opcode1, u32 crn, u32 crm, u32 opcode2, u32 value);
ARMBus& m_bus;
std::array<u32, 16> m_registers{};
u32 m_cpsr = 0;
// User/System bank, FIQ bank, and r13/r14 banks for the remaining privileged
// modes.
std::array<u32, 7> m_user_r8_r14{};
std::array<u32, 7> m_fiq_r8_r14{};
std::array<u32, 2> m_irq_r13_r14{};
std::array<u32, 2> m_svc_r13_r14{};
std::array<u32, 2> m_abt_r13_r14{};
std::array<u32, 2> m_und_r13_r14{};
u32 m_spsr_fiq = 0;
u32 m_spsr_irq = 0;
u32 m_spsr_svc = 0;
u32 m_spsr_abt = 0;
u32 m_spsr_und = 0;
CP15State m_cp15{};
// ARM926 has separate instruction/data TLBs. A unified 1 KiB-granularity
// software cache keeps the observable translations while avoiding a
// page-table walk on every interpreted access.
mutable std::array<TLBEntry, TLB_ENTRY_COUNT> m_tlb{};
std::array<InstructionCacheEntry, INSTRUCTION_CACHE_ENTRY_COUNT> m_instruction_cache{};
mutable u32 m_tlb_generation = 1;
u32 m_instruction_cache_generation = 1;
bool m_big_endian = true;
bool m_irq_line = false;
bool m_fiq_line = false;
bool m_waiting_for_interrupt = false;
bool m_waiting_for_memory_poll = false;
bool m_yield_requested = false;
u32 m_memory_poll_address = 0;
bool m_pc_written = false;
u32 m_instruction_address = 0;
u32 m_last_undefined_instruction = 0;
u64 m_executed_instructions = 0;
};
} // namespace IOS::LLE
+153
View File
@@ -0,0 +1,153 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/IOS/Starlet/Starlet.h"
#include <algorithm>
#include <memory>
#include "Common/ChunkFile.h"
#include "Common/Logging/Log.h"
#include "Core/CoreTiming.h"
#include "Core/HW/WII_IPC.h"
#include "Core/IOS/Starlet/ARMCore.h"
#include "Core/IOS/Starlet/StarletMemory.h"
#include "Core/System.h"
namespace IOS::LLE
{
Starlet::Starlet(Core::System& system) : m_system(system)
{
}
Starlet::~Starlet() = default;
bool Starlet::Init(const std::string& dump_directory, std::string* error)
{
// Construct the complete physical bus (including its persistent controller
// reset state) before the CPU so the ARM core always observes native-width
// memory accesses and the same Hollywood device state.
m_memory = std::make_unique<StarletMemory>(m_system);
if (!m_memory->Init(dump_directory, error))
{
m_memory.reset();
return false;
}
// ARMCore owns generation-tagged software TLB and instruction caches, so
// construct it only after the complete physical bus exists.
m_core = std::make_unique<ARMCore>(*m_memory);
m_core->Reset(StarletMemory::BOOT_ROM_BASE);
m_run_event = m_system.GetCoreTiming().RegisterEvent("StarletLLE", RunCallback);
m_initialized = true;
m_system.GetCoreTiming().ScheduleEvent(0, m_run_event);
INFO_LOG_FMT(IOS, "Starlet LLE started at PC {:#010x}", m_core->GetRegister(15));
return true;
}
void Starlet::Shutdown()
{
if (m_run_event)
m_system.GetCoreTiming().RemoveEvent(m_run_event);
m_initialized = false;
m_run_event = nullptr;
m_core.reset();
m_memory.reset();
}
void Starlet::DoState(PointerWrap& p)
{
p.Do(m_initialized);
if (!m_initialized)
return;
m_memory->DoState(p);
m_core->DoState(p);
}
u32 Starlet::GetPC() const
{
return m_core ? m_core->GetRegister(15) : 0;
}
u64 Starlet::GetExecutedInstructions() const
{
return m_core ? m_core->GetExecutedInstructions() : 0;
}
std::optional<u32> Starlet::TryReadBroadwayResetInstruction(u32 address) const
{
return m_memory ? m_memory->TryReadBroadwayResetInstruction(address) : std::nullopt;
}
u8 Starlet::ReadHollywoodBus8(u32 address)
{
return m_memory ? m_memory->Read8(address) : 0;
}
u16 Starlet::ReadHollywoodBus16(u32 address)
{
return m_memory ? m_memory->Read16(address) : 0;
}
u32 Starlet::ReadHollywoodRegister(u32 address)
{
return m_memory ? m_memory->Read32(address) : 0;
}
void Starlet::WriteHollywoodBus8(u32 address, u8 value)
{
if (m_memory)
m_memory->Write8(address, value);
}
void Starlet::WriteHollywoodBus16(u32 address, u16 value)
{
if (m_memory)
m_memory->Write16(address, value);
}
void Starlet::WriteHollywoodRegister(u32 address, u32 value)
{
if (m_memory)
m_memory->Write32(address, value);
}
void Starlet::SetWiimoteSource(size_t index, WiimoteCommon::HIDWiimote* source)
{
if (m_memory)
m_memory->SetWiimoteSource(index, source);
}
void Starlet::YieldForIPC()
{
if (m_core)
m_core->RequestYield();
}
void Starlet::RunCallback(Core::System& system, u64, s64 cycles_late)
{
if (Starlet* starlet = system.GetStarlet())
starlet->RunSlice(cycles_late);
}
void Starlet::RunSlice(s64 cycles_late)
{
if (!m_initialized)
return;
m_core->SetIRQLine(m_system.GetWiiIPC().IsStarletIRQAsserted());
m_core->SetFIQLine(m_system.GetWiiIPC().IsStarletFIQAsserted());
// Active and sleeping states use different scheduling quanta so busy IOS code
// amortizes host callbacks while a sleeping core retains prompt
// external-interrupt wakeups.
const bool ipc_handshake_active = (m_system.GetWiiIPC().ReadStarletRegister(0x0c) & 0x0f) != 0;
const u64 arm_cycles =
m_core->IsWaitingForExternalEvent() ?
ARM_IDLE_SLICE_CYCLES :
(ipc_handshake_active ? ARM_IPC_SLICE_CYCLES : ARM_ACTIVE_SLICE_CYCLES);
m_core->RunCycles(arm_cycles);
const u64 broadway_cycles = arm_cycles * BROADWAY_CLOCK / ARM_CLOCK;
const s64 next = std::max<s64>(1, static_cast<s64>(broadway_cycles) - cycles_late);
m_system.GetCoreTiming().ScheduleEvent(next, m_run_event);
}
} // namespace IOS::LLE
+84
View File
@@ -0,0 +1,84 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <optional>
#include <string>
#include "Common/CommonTypes.h"
class PointerWrap;
namespace Core
{
class System;
}
namespace CoreTiming
{
struct EventType;
}
namespace WiimoteCommon
{
class HIDWiimote;
}
namespace IOS::LLE
{
class ARMCore;
class StarletMemory;
class Starlet final
{
public:
explicit Starlet(Core::System& system);
~Starlet();
Starlet(const Starlet&) = delete;
Starlet& operator=(const Starlet&) = delete;
bool Init(const std::string& dump_directory, std::string* error);
void Shutdown();
void DoState(PointerWrap& p);
bool IsInitialized() const { return m_initialized; }
u32 GetPC() const;
u64 GetExecutedInstructions() const;
std::optional<u32> TryReadBroadwayResetInstruction(u32 address) const;
u8 ReadHollywoodBus8(u32 address);
u16 ReadHollywoodBus16(u32 address);
u32 ReadHollywoodRegister(u32 address);
void WriteHollywoodBus8(u32 address, u8 value);
void WriteHollywoodBus16(u32 address, u16 value);
void WriteHollywoodRegister(u32 address, u32 value);
void SetWiimoteSource(size_t index, WiimoteCommon::HIDWiimote* source);
void YieldForIPC();
private:
static constexpr u64 ARM_CLOCK = 243000000;
static constexpr u64 BROADWAY_CLOCK = 729000000;
// A 4096-cycle active slice is about 16.9 us at 243 MHz. This keeps IPC/timer
// wake latency well below one USB frame while amortizing CoreTiming and
// device-clock work across ARM instructions.
static constexpr u64 ARM_ACTIVE_SLICE_CYCLES = 4096;
// During an IPC handshake, use a finer interleave to approximate the two
// physical CPUs running concurrently. This is the last known-good boundary
// for the original IOS and avoids recursively executing Starlet from a
// Broadway MMIO callback.
static constexpr u64 ARM_IPC_SLICE_CYCLES = 256;
// WFI keeps hardware clocks running without issuing instructions. A 100 us
// idle quantum avoids hundreds of thousands of scheduler callbacks per second
// while bounding interrupt wake latency.
static constexpr u64 ARM_IDLE_SLICE_CYCLES = ARM_CLOCK / 10000;
static void RunCallback(Core::System& system, u64 userdata, s64 cycles_late);
void RunSlice(s64 cycles_late);
Core::System& m_system;
std::unique_ptr<StarletMemory> m_memory;
std::unique_ptr<ARMCore> m_core;
CoreTiming::EventType* m_run_event = nullptr;
bool m_initialized = false;
};
} // namespace IOS::LLE
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,273 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <cstddef>
#include <deque>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "Common/CommonTypes.h"
#include "Common/IOFile.h"
#include "Core/IOS/Starlet/ARMCore.h"
#include "Core/IOS/USB/Bluetooth/WiimoteDevice.h"
class PointerWrap;
namespace Core
{
class System;
}
namespace IOS::LLE
{
// Starlet's physical address space and the first hardware devices required by the immutable Wii
// boot ROM. NAND is deliberately opened read-only; program/erase commands use a copy-on-write
// overlay so an experimental LLE session can never modify the user's console backup.
class StarletMemory final : public ARMBus, public IOS::HLE::WiimoteDeviceHost
{
public:
static constexpr u32 BOOT_ROM_BASE = 0xffff0000;
static constexpr u32 BOOT_ROM_SIZE = 0x2000;
static constexpr u32 SRAM_BASE = 0x0d400000;
static constexpr u32 SRAM_SIZE = 0x18000;
static constexpr u32 SRAM_WINDOW_SIZE = 0x20000;
static constexpr u32 SRAM_MIRROR_BASE = 0xfff00000;
explicit StarletMemory(Core::System& system);
~StarletMemory() override
{
for (auto& wiimote : m_wiimotes)
{
if (wiimote)
wiimote->SetSource(nullptr);
}
}
bool Init(const std::string& dump_directory, std::string* error);
void Reset();
void DoState(PointerWrap& p);
u8 Read8(u32 address) override;
u16 Read16(u32 address) override;
u32 Read32(u32 address) override;
void Write8(u32 address, u8 value) override;
void Write16(u32 address, u16 value) override;
void Write32(u32 address, u32 value) override;
void AdvanceCycles(u64 cycles) override;
bool IsIdlePollAddress(u32 address, u32 size) const override;
u64 GetCycles() const { return m_arm_cycles; }
std::optional<u32> TryReadBroadwayResetInstruction(u32 address) const;
void SetWiimoteSource(size_t index, WiimoteCommon::HIDWiimote* source);
private:
static constexpr u32 NAND_PAGE_DATA_SIZE = 0x800;
static constexpr u32 NAND_PAGE_SPARE_SIZE = 0x40;
static constexpr u32 NAND_RAW_PAGE_SIZE = NAND_PAGE_DATA_SIZE + NAND_PAGE_SPARE_SIZE;
static constexpr u32 NAND_PAGES_PER_BLOCK = 0x40;
static constexpr u32 NAND_PAGE_COUNT = 0x40000;
using NANDPage = std::array<u8, NAND_RAW_PAGE_SIZE>;
static bool IsMemoryAddress(u32 address);
static bool IsSRAMWindowAddress(u32 address);
static bool IsParityOdd(u8 value);
static std::array<u8, 16> CalculateNANDECC(const u8* data);
static u32 RotateLeft(u32 value, u32 amount);
u8 ReadMapped8(u32 address) const;
void WriteMapped8(u32 address, u8 value);
bool IsBootROMAddress(u32 address) const;
u32 GetBootROMOffset(u32 address) const;
u32 GetSRAMOffset(u32 address) const;
u32 ReadRegister(u32 address) const;
void WriteRegister(u32 address, u32 value);
bool IsMemoryControllerIndirectRegister(u32 address) const;
u16 ReadMemoryControllerHalfword(u32 address) const;
void WriteMemoryControllerHalfword(u32 address, u16 value);
u16 ReadDDRRegister(u16 address) const;
void WriteDDRRegister(u16 address, u16 value);
u16 ReadBISTRegister(u16 address) const;
void WriteBISTRegister(u16 address, u16 value);
u32 ReadSDHCRegister(u32 address) const;
void HandleSDHCWrite(u32 address);
u32 ReadWiFiSDIORegister(u32 address) const;
void HandleWiFiSDIOWrite(u32 address);
void ExecuteWiFiSDIOCommand(u32 command_and_mode);
u8 ReadWiFiSDIOByte(u32 function, u32 address) const;
void WriteWiFiSDIOByte(u32 function, u32 address, u8 value);
u32 GetWiFiSDIOBackplaneAddress(u32 address) const;
void SetWiFiSDIOInterruptStatus(u16 normal_status, u16 error_status = 0);
void UpdateWiFiSDIOInterrupt();
void InitSDCard();
void ExecuteSDHCCommand(u32 command_and_mode);
bool TransferSDCardBlocks(bool read, u32 argument, u32 block_size, u32 block_count,
u32 dma_address);
std::array<u32, 4> GetSDCardCSD() const;
void SetSDHCInterruptStatus(u16 normal_status, u16 error_status = 0);
void UpdateSDHCInterrupt();
static bool IsEHCIAddress(u32 address);
u32 ReadEHCIRegister(u32 address) const;
void HandleEHCIWrite(u32 address);
void ResetEHCIController(bool preserve_phy_registers = false);
void SetEHCIInterruptStatus(u32 status);
void UpdateEHCIInterrupt();
void AdvanceEHCI(u64 cycles);
static std::optional<size_t> GetOHCIControllerIndex(u32 address);
u32 ReadOHCIRegister(size_t controller, u32 address) const;
void HandleOHCIWrite(size_t controller, u32 address);
void ResetOHCIController(size_t controller);
void SetOHCIInterruptStatus(size_t controller, u32 status);
void UpdateOHCIInterrupt(size_t controller);
void AdvanceOHCI(size_t controller, u64 cycles);
u32 ReadOHCIMemory32(u32 address) const;
void WriteOHCIMemory32(u32 address, u32 value);
std::vector<u8> ReadOHCIBuffer(u32 current_buffer, u32 buffer_end) const;
void WriteOHCIBuffer(u32 current_buffer, u32 buffer_end, const u8* data, size_t size);
void ProcessOHCISchedules(size_t controller);
bool ProcessOHCIEndpoint(size_t controller, u32 endpoint_address, bool* interrupt_due);
enum class OHCITransferResult
{
Completed,
Pending,
Stalled,
};
OHCITransferResult ExecuteOHCITransfer(size_t controller, u32 endpoint, u32 direction,
std::vector<u8>* buffer, size_t* actual_length);
std::vector<u8> GetOHCI1Descriptor(u16 value) const;
void ExecuteOHCI1HCICommand(const std::vector<u8>& command);
void QueueOHCI1HCIEvent(u8 event_code, const void* parameters, size_t parameter_size);
void QueueOHCI1HCICommandComplete(u16 opcode, const std::vector<u8>& return_parameters);
void QueueOHCI1HCICommandStatus(u16 opcode, u8 status = 0);
void QueueOHCI1ConnectionComplete(const bdaddr_t& address, u8 status);
void QueueOHCI1CompletedPackets(u16 connection_handle, u16 packet_count);
void ExecuteOHCI1ACLPacket(std::vector<u8>* packet);
void InitializeWiimotes();
void UpdateWiimotes();
u16 GetWiimoteConnectionHandle(const bdaddr_t& address) const;
IOS::HLE::WiimoteDevice* AccessWiimote(const bdaddr_t& address);
IOS::HLE::WiimoteDevice* AccessWiimote(u16 connection_handle);
void SendACLPacket(const bdaddr_t& source, const u8* data, u32 size) override;
bool RemoteConnect(IOS::HLE::WiimoteDevice& wiimote) override;
bool RemoteDisconnect(const bdaddr_t& address) override;
bool IsSensorBarEnabled() const override;
void HandleNANDCommand(u32 command);
void CompleteNANDCommand(u32 command);
bool ReadRawNANDPage(u32 page, NANDPage* raw);
bool InstallEmulatedWiimotePairings();
bool ReadNANDPage(u32 command);
bool ReadNANDID(u32 command);
bool ReadNANDStatus(u32 command);
bool StageNANDProgram(u32 command, bool random_data_input);
bool CommitNANDProgram();
bool StageNANDErase();
bool CommitNANDErase();
void ResetNANDOperationState();
void HandleAESWrite(u32 address, u32 value);
void ExecuteAESCommand(u32 command);
void HandleSHAWrite(u32 address, u32 value);
void ExecuteSHACommand(u32 command);
void CompressSHA1(const u8* block);
void HandleOTPCommand(u32 command);
void HandleGPIOWrite(u32 value);
u32 GetTimer() const;
Core::System& m_system;
std::array<u8, BOOT_ROM_SIZE> m_boot_rom{};
std::array<u8, 0x400> m_keys{};
std::array<u8, SRAM_SIZE> m_sram{};
File::IOFile m_nand;
File::IOFile m_sd_card;
std::map<u32, u8> m_registers;
std::map<u32, NANDPage> m_nand_overlay;
NANDPage m_nand_program_data{};
u32 m_nand_control_before_write = 0;
u32 m_nand_read_page = 0;
u32 m_nand_read_column = 0;
u32 m_nand_program_page = 0;
u32 m_nand_erase_page = 0;
u8 m_nand_status = 0xc0;
bool m_nand_read_pending = false;
bool m_nand_program_pending = false;
bool m_nand_program_ecc_enabled = false;
bool m_nand_erase_pending = false;
u32 m_nand_pending_command = 0;
u64 m_nand_cycles_until_completion = 0;
bool m_sd_card_inserted = false;
bool m_sd_card_high_capacity = false;
bool m_sd_app_command = false;
bool m_sd_card_selected = false;
u32 m_sd_block_length = 512;
u32 m_sdhc_status_before_write = 0;
u32 m_wifi_sdio_status_before_write = 0;
std::map<u32, u8> m_wifi_sdio_registers;
std::deque<u8> m_wifi_sdio_pio_read_data;
u32 m_wifi_sdio_pio_write_function = 0;
u32 m_wifi_sdio_pio_write_address = 0;
u32 m_wifi_sdio_pio_write_remaining = 0;
bool m_wifi_sdio_pio_write_increment = false;
bool m_wifi_sdio_selected = false;
u32 m_ehci_interrupt_enable = 0;
u32 m_ehci_status_before_write = 0;
u32 m_usb_force_reset_before_write = 0;
u64 m_ehci_microframe_cycle_remainder = 0;
bool m_ehci_running = false;
std::array<u32, 2> m_ohci_interrupt_enable{};
std::array<u32, 2> m_ohci_interrupt_status_before_write{};
std::array<std::array<u32, 2>, 2> m_ohci_port_status_before_write{};
std::array<std::array<u8, 2>, 2> m_ohci_port_reset_frames{};
std::array<u64, 2> m_ohci_frame_cycle_remainder{};
std::array<bool, 2> m_ohci_operational{};
u16 m_ohci1_attach_delay_frames = 0;
std::array<u8, 8> m_ohci1_setup_packet{};
std::vector<u8> m_ohci1_control_response;
std::deque<std::vector<u8>> m_ohci1_hci_events;
std::deque<std::vector<u8>> m_ohci1_acl_packets;
u8 m_ohci1_acl_delay_frames = 0;
std::array<std::unique_ptr<IOS::HLE::WiimoteDevice>, 5> m_wiimotes;
std::array<bdaddr_t, 5> m_wiimote_addresses{};
u64 m_wiimote_cycle_remainder = 0;
std::array<u16, 5> m_last_wiimote_buttons{};
u8 m_ohci1_scan_enable = 0;
u8 m_ohci1_device_address = 0;
u8 m_ohci1_pending_address = 0xff;
u8 m_ohci1_configuration = 0;
bool m_ohci1_control_request_valid = false;
bool m_ohci1_control_stalled = false;
bool m_ohci1_hci_command_pending = false;
std::array<u8, 16> m_aes_key{};
std::array<u8, 16> m_aes_iv{};
std::array<u32, 5> m_sha_state{};
std::array<u16, 0x400> m_ddr_registers{};
std::array<u16, 0x100> m_ddr_seq_registers{};
std::array<u16, 0x100> m_ddr_bist_registers{};
std::array<u8, 0x100> m_seeprom{};
u16 m_ddr_register_address = 0;
u16 m_ddr_seq_address = 0;
u16 m_ddr_bist_address = 0;
u32 m_gpio_out = 0;
u16 m_seeprom_command = 0;
u16 m_seeprom_output = 0;
u16 m_seeprom_input = 0;
u8 m_seeprom_command_bits = 0;
u8 m_seeprom_output_bits = 0;
u8 m_seeprom_input_bits = 0;
u8 m_seeprom_write_address = 0;
bool m_seeprom_miso = false;
bool m_seeprom_write_enabled = false;
bool m_seeprom_write_pending = false;
bool m_seeprom_write_all = false;
u64 m_arm_cycles = 0;
bool m_initialized = false;
bool m_boot0_mapped = true;
bool m_sram_split_mode = false;
};
} // namespace IOS::LLE
+58 -2
View File
@@ -3,6 +3,10 @@
#include "Core/IOS/USB/Bluetooth/BTBase.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <iterator>
#include <string>
#include <vector>
@@ -11,10 +15,32 @@
#include "Common/FileUtil.h"
#include "Common/IOFile.h"
#include "Common/Logging/Log.h"
#include "Core/HW/Wiimote.h"
#include "Core/SysConf.h"
namespace IOS::HLE
{
namespace
{
#pragma pack(push, 1)
constexpr size_t CONF_PAD_MAX_REGISTERED = 10;
struct ConfPadDevice
{
u8 bdaddr[6];
char name[0x40];
};
struct ConfPads
{
u8 num_registered;
ConfPadDevice registered[CONF_PAD_MAX_REGISTERED];
ConfPadDevice active[MAX_BBMOTES];
ConfPadDevice unknown;
};
#pragma pack(pop)
} // namespace
void BackUpBTInfoSection(const SysConf* sysconf)
{
const std::string filename = File::GetUserPath(D_CONFIG_IDX) + DIR_SEP WII_BTDINF_BACKUP;
@@ -38,14 +64,44 @@ void RestoreBTInfoSection(SysConf* sysconf)
File::IOFile backup(filename, "rb");
if (!backup)
return;
auto& section = sysconf->GetOrAddEntry("BT.DINF", SysConf::Entry::Type::BigArray)->bytes;
if (!backup.ReadBytes(section.data(), section.size()))
std::vector<u8> restored_section(backup.GetSize());
if (!backup.ReadBytes(restored_section.data(), restored_section.size()))
{
ERROR_LOG_FMT(IOS_WIIMOTE, "Failed to read backed up BT.DINF section");
return;
}
auto& section = sysconf->GetOrAddEntry("BT.DINF", SysConf::Entry::Type::BigArray)->bytes;
section = std::move(restored_section);
}
File::Delete(filename);
}
std::vector<u8> GetEmulatedWiimotePairingData()
{
ConfPads bt_dinf{};
for (u8 i = 0; i != MAX_BBMOTES; ++i)
{
const std::array<u8, 6> address = {0x11, 0x02, 0x19, 0x79, 0, i};
std::ranges::copy(address, std::rbegin(bt_dinf.active[i].bdaddr));
std::ranges::copy(address, std::rbegin(bt_dinf.registered[i].bdaddr));
const char* const name =
i == WIIMOTE_BALANCE_BOARD ? "Nintendo RVL-WBC-01" : "Nintendo RVL-CNT-01";
std::memcpy(bt_dinf.registered[i].name, name, 20);
std::memcpy(bt_dinf.active[i].name, name, 20);
}
bt_dinf.num_registered = MAX_BBMOTES;
std::vector<u8> data(sizeof(bt_dinf));
std::memcpy(data.data(), &bt_dinf, sizeof(bt_dinf));
return data;
}
void PopulateEmulatedWiimotePairings(SysConf* sysconf)
{
auto& section = sysconf->GetOrAddEntry("BT.DINF", SysConf::Entry::Type::BigArray)->bytes;
section = GetEmulatedWiimotePairingData();
}
} // namespace IOS::HLE
@@ -3,6 +3,8 @@
#pragma once
#include <vector>
#include "Common/CommonTypes.h"
#include "Core/IOS/Device.h"
#include "Core/IOS/IOS.h"
@@ -26,6 +28,8 @@ static void DoStateForMessage(EmulationKernel& ios, PointerWrap& p, std::unique_
void BackUpBTInfoSection(const SysConf* sysconf);
void RestoreBTInfoSection(SysConf* sysconf);
std::vector<u8> GetEmulatedWiimotePairingData();
void PopulateEmulatedWiimotePairings(SysConf* sysconf);
class BluetoothBaseDevice : public EmulationDevice
{
+9 -18
View File
@@ -16,6 +16,7 @@
#include "Core/Debugger/Debugger_SymbolMap.h"
#include "Core/HW/Memmap.h"
#include "Core/HW/SystemTimers.h"
#include "Core/HW/WII_IPC.h"
#include "Core/HW/Wiimote.h"
#include "Core/HW/WiimoteEmu/DesiredWiimoteState.h"
#include "Core/IOS/Device.h"
@@ -42,37 +43,22 @@ BluetoothEmuDevice::BluetoothEmuDevice(EmulationKernel& ios, const std::string&
if (!Core::WantsDeterminism())
BackUpBTInfoSection(&sysconf);
ConfPads bt_dinf{};
PopulateEmulatedWiimotePairings(&sysconf);
for (u8 i = 0; i != MAX_BBMOTES; ++i)
{
// Note: BluetoothEmu::GetConnectionHandle and WiimoteDevice::GetNumber rely on final byte.
const bdaddr_t tmp_bd = {0x11, 0x02, 0x19, 0x79, 0, i};
// Previous records can be safely overwritten, since they are backed up
std::ranges::copy(tmp_bd, std::rbegin(bt_dinf.active[i].bdaddr));
std::ranges::copy(tmp_bd, std::rbegin(bt_dinf.registered[i].bdaddr));
const auto& wm_name =
(i == WIIMOTE_BALANCE_BOARD) ? "Nintendo RVL-WBC-01" : "Nintendo RVL-CNT-01";
memcpy(bt_dinf.registered[i].name, wm_name, 20);
memcpy(bt_dinf.active[i].name, wm_name, 20);
DEBUG_LOG_FMT(IOS_WIIMOTE, "Wii Remote {} BT ID {:x},{:x},{:x},{:x},{:x},{:x}", i, tmp_bd[0],
tmp_bd[1], tmp_bd[2], tmp_bd[3], tmp_bd[4], tmp_bd[5]);
const unsigned int hid_source_number =
NetPlay::IsNetPlayRunning() ? NetPlay::NetPlay_GetLocalWiimoteForSlot(i) : i;
m_wiimotes[i] = std::make_unique<WiimoteDevice>(this, tmp_bd, hid_source_number);
m_wiimotes[i] = std::make_unique<WiimoteDevice>(this, tmp_bd, hid_source_number, i);
}
bt_dinf.num_registered = MAX_BBMOTES;
// save now so that when games load sysconf file it includes the new Wii Remotes
// and the correct order for connected Wii Remotes
auto& section = sysconf.GetOrAddEntry("BT.DINF", SysConf::Entry::Type::BigArray)->bytes;
section.resize(sizeof(ConfPads));
std::memcpy(section.data(), &bt_dinf, sizeof(ConfPads));
// Save now so that emulated software sees the paired Wii Remotes and their active order.
if (!sysconf.Save())
PanicAlertFmtT("Failed to write BT.DINF to SYSCONF");
}
@@ -119,6 +105,11 @@ bool BluetoothEmuDevice::RemoteDisconnect(const bdaddr_t& address)
return SendEventDisconnect(GetConnectionHandle(address), 0x13);
}
bool BluetoothEmuDevice::IsSensorBarEnabled() const
{
return GetSystem().GetWiiIPC().GetGPIOOutFlags()[IOS::GPIO::SENSOR_BAR] ? true : false;
}
std::optional<IPCReply> BluetoothEmuDevice::Close(u32 fd)
{
// Clean up state
+5 -22
View File
@@ -36,7 +36,7 @@ struct SQueuedEvent
// /dev/usb/oh1 -> internal usb bus
// 57e/305 -> VendorID/ProductID of device on usb bus
// This device is ONLY the internal Bluetooth module (based on BCM2045 chip)
class BluetoothEmuDevice final : public BluetoothBaseDevice
class BluetoothEmuDevice final : public BluetoothBaseDevice, public WiimoteDeviceHost
{
public:
BluetoothEmuDevice(EmulationKernel& ios, const std::string& device_name);
@@ -49,11 +49,12 @@ public:
void Update() override;
// Send ACL data back to Bluetooth stack
void SendACLPacket(const bdaddr_t& source, const u8* data, u32 size);
void SendACLPacket(const bdaddr_t& source, const u8* data, u32 size) override;
// Returns true if controller is configured to see the connection request.
bool RemoteConnect(WiimoteDevice&);
bool RemoteDisconnect(const bdaddr_t& address);
bool RemoteConnect(WiimoteDevice&) override;
bool RemoteDisconnect(const bdaddr_t& address) override;
bool IsSensorBarEnabled() const override;
WiimoteDevice* AccessWiimoteByIndex(std::size_t index);
@@ -177,23 +178,5 @@ private:
// OGF 0x3F - Vendor specific
void CommandVendorSpecific_FC4C(u32 input_address, u32 size);
void CommandVendorSpecific_FC4F(u32 input_address, u32 size);
#pragma pack(push, 1)
#define CONF_PAD_MAX_REGISTERED 10
struct ConfPadDevice
{
u8 bdaddr[6];
char name[0x40];
};
struct ConfPads
{
u8 num_registered;
ConfPadDevice registered[CONF_PAD_MAX_REGISTERED];
ConfPadDevice active[MAX_BBMOTES];
ConfPadDevice unknown;
};
#pragma pack(pop)
};
} // namespace IOS::HLE
@@ -15,16 +15,13 @@
#include "Common/MsgHandler.h"
#include "Common/Swap.h"
#include "Core/Core.h"
#include "Core/HW/WII_IPC.h"
#include "Core/HW/Wiimote.h"
#include "Core/HW/WiimoteCommon/WiimoteConstants.h"
#include "Core/HW/WiimoteCommon/WiimoteHid.h"
#include "Core/HW/WiimoteEmu/DesiredWiimoteState.h"
#include "Core/Host.h"
#include "Core/IOS/USB/Bluetooth/BTEmu.h"
#include "Core/IOS/USB/Bluetooth/WiimoteHIDAttr.h"
#include "Core/IOS/USB/Bluetooth/l2cap.h"
#include "Core/System.h"
namespace IOS::HLE
{
@@ -54,8 +51,9 @@ private:
constexpr int CONNECTION_MESSAGE_TIME = 3000;
WiimoteDevice::WiimoteDevice(BluetoothEmuDevice* host, bdaddr_t bd, unsigned int hid_source_number)
: m_host(host), m_bd(bd),
WiimoteDevice::WiimoteDevice(WiimoteDeviceHost* host, bdaddr_t bd, unsigned int hid_source_number,
unsigned int device_number)
: m_host(host), m_number(device_number), m_bd(bd),
m_name(GetNumber() == WIIMOTE_BALANCE_BOARD ? "Nintendo RVL-WBC-01" : "Nintendo RVL-CNT-01")
{
@@ -105,6 +103,7 @@ void WiimoteDevice::DoState(PointerWrap& p)
{
p.Do(m_baseband_state);
p.Do(m_hid_state);
p.Do(m_number);
p.Do(m_bd);
p.Do(m_class);
p.Do(m_features);
@@ -118,19 +117,20 @@ void WiimoteDevice::DoState(PointerWrap& p)
u32 WiimoteDevice::GetNumber() const
{
return GetBD().back();
return m_number;
}
bool WiimoteDevice::IsInquiryScanEnabled() const
{
// Our Wii Remote is conveniently discoverable as long as it's enabled and doesn't have a
// baseband connection.
// Our Wii Remote is conveniently discoverable as long as it's enabled and
// doesn't have a baseband connection.
return !IsConnected() && IsSourceValid();
}
bool WiimoteDevice::IsPageScanEnabled() const
{
// Our Wii Remote will accept a connection as long as it isn't currently connected.
// Our Wii Remote will accept a connection as long as it isn't currently
// connected.
return !IsConnected() && IsSourceValid();
}
@@ -265,16 +265,29 @@ bool WiimoteDevice::EventConnectionAccept()
return true;
}
void WiimoteDevice::EventControllerReset()
{
if (!IsConnected())
return;
Reset();
if (IsSourceValid())
SetBasebandState(BasebandState::RequestConnection);
}
void WiimoteDevice::EventDisconnect(u8 reason)
{
// If someone wants to be fancy we could also figure out the values for reason
// and display things like "Wii Remote %i disconnected due to inactivity!" etc.
// FYI: It looks like reason is always 0x13 (User Ended Connection).
// and display things like "Wii Remote %i disconnected due to inactivity!"
// etc. FYI: It looks like reason is always 0x13 (User Ended Connection).
Core::DisplayMessage(
fmt::format("Wii Remote {} disconnected by emulated software", GetNumber() + 1),
CONNECTION_MESSAGE_TIME);
INFO_LOG_FMT(IOS_WIIMOTE, "Wii Remote {} disconnected with HCI reason {:#04x}", GetNumber(),
reason);
Reset();
}
@@ -308,7 +321,8 @@ void WiimoteDevice::Update()
{
if (m_host->RemoteConnect(*this))
{
// After a connection request is visible to the controller switch to inactive.
// After a connection request is visible to the controller switch to
// inactive.
SetBasebandState(BasebandState::Inactive);
}
}
@@ -335,7 +349,7 @@ void WiimoteDevice::Update()
{
if (LinkChannel(L2CAP_PSM_HID_CNTL) && LinkChannel(L2CAP_PSM_HID_INTR))
{
DEBUG_LOG_FMT(IOS_WIIMOTE, "HID linking is complete.");
INFO_LOG_FMT(IOS_WIIMOTE, "Wii Remote {} HID linking is complete", GetNumber());
m_hid_state = HIDState::Inactive;
}
}
@@ -352,7 +366,8 @@ WiimoteDevice::PrepareInput(WiimoteEmu::DesiredWiimoteState* wiimote_state)
if (m_baseband_state == BasebandState::Inactive)
{
// Allow button press to trigger activation after a second of no connection activity.
// Allow button press to trigger activation after a second of no connection
// activity.
if (!m_connection_request_counter)
{
wiimote_state->buttons = m_hid_source->GetCurrentlyPressedButtons();
@@ -365,9 +380,8 @@ WiimoteDevice::PrepareInput(WiimoteEmu::DesiredWiimoteState* wiimote_state)
const auto* channel = FindChannelWithPSM(L2CAP_PSM_HID_INTR);
if (channel && channel->IsComplete())
{
auto gpio_out = m_host->GetSystem().GetWiiIPC().GetGPIOOutFlags();
m_hid_source->PrepareInput(wiimote_state,
gpio_out[IOS::GPIO::SENSOR_BAR] ?
m_host->IsSensorBarEnabled() ?
WiimoteCommon::HIDWiimote::SensorBarState::Enabled :
WiimoteCommon::HIDWiimote::SensorBarState::Disabled);
return NextUpdateInputCall::Update;
@@ -448,7 +462,8 @@ void WiimoteDevice::ExecuteL2capCmd(u8* ptr, u32 size)
m_host->SendACLPacket(GetBD(), reinterpret_cast<const u8*>(&data_frame), sizeof(data_frame));
// Does the wii remote reply on the control or interrupt channel in this situation?
// Does the wii remote reply on the control or interrupt channel in this
// situation?
m_hid_source->InterruptDataOutput(data + sizeof(hid_type), data_size - sizeof(hid_type));
}
else
@@ -20,7 +20,21 @@ struct DesiredWiimoteState;
namespace IOS::HLE
{
class BluetoothEmuDevice;
class WiimoteDevice;
// Transport implemented by both the IOS HLE Bluetooth device and hardware-level Bluetooth
// controllers. WiimoteDevice owns the remote-side L2CAP/HID state and does not need to know which
// IOS implementation is driving the HCI transport.
class WiimoteDeviceHost
{
public:
virtual ~WiimoteDeviceHost() = default;
virtual void SendACLPacket(const bdaddr_t& source, const u8* data, u32 size) = 0;
virtual bool RemoteConnect(WiimoteDevice& wiimote) = 0;
virtual bool RemoteDisconnect(const bdaddr_t& address) = 0;
virtual bool IsSensorBarEnabled() const = 0;
};
class WiimoteDevice
{
@@ -29,7 +43,8 @@ public:
using FeaturesType = std::array<u8, HCI_FEATURES_SIZE>;
using LinkKeyType = std::array<u8, HCI_KEY_SIZE>;
WiimoteDevice(BluetoothEmuDevice* host, bdaddr_t bd, unsigned int hid_source_number);
WiimoteDevice(WiimoteDeviceHost* host, bdaddr_t bd, unsigned int hid_source_number,
unsigned int device_number);
~WiimoteDevice();
WiimoteDevice(const WiimoteDevice&) = delete;
@@ -73,6 +88,7 @@ public:
bool EventConnectionAccept();
bool EventConnectionRequest();
void EventControllerReset();
void EventDisconnect(u8 reason);
// nullptr may be passed to disable the remote.
@@ -125,12 +141,13 @@ private:
using ChannelMap = std::map<u16, SChannel>;
BluetoothEmuDevice* m_host;
WiimoteDeviceHost* m_host;
WiimoteCommon::HIDWiimote* m_hid_source = nullptr;
// State to save:
BasebandState m_baseband_state = BasebandState::Inactive;
HIDState m_hid_state = HIDState::Inactive;
u32 m_number;
bdaddr_t m_bd;
ClassType m_class;
FeaturesType m_features;
+75 -8
View File
@@ -50,6 +50,9 @@
#include "Core/HW/MMIO.h"
#include "Core/HW/Memmap.h"
#include "Core/HW/ProcessorInterface.h"
#include "Core/HW/WII_IPC.h"
#include "Core/IOS/Starlet/Starlet.h"
#include "Core/IOS/Starlet/StarletMemory.h"
#include "Core/PowerPC/GDBStub.h"
#include "Core/PowerPC/JitInterface.h"
#include "Core/PowerPC/PowerPC.h"
@@ -230,6 +233,23 @@ T MMU::ReadFromHardware(u32 em_address)
wi = translated_addr.wi;
}
// Hollywood exposes Starlet's 128 KiB SRAM aperture to Broadway at 0x0d400000. This region is
// outside the 0x0d00/0x0d80 MMIO blocks and must not alias IPC registers through MMIO::UniqueID.
if (em_address >= IOS::LLE::StarletMemory::SRAM_BASE &&
em_address <= IOS::LLE::StarletMemory::SRAM_BASE + IOS::LLE::StarletMemory::SRAM_WINDOW_SIZE -
sizeof(T))
{
if (IOS::LLE::Starlet* const starlet = m_system.GetStarlet())
{
if constexpr (sizeof(T) == sizeof(u8))
return static_cast<T>(starlet->ReadHollywoodBus8(em_address));
if constexpr (sizeof(T) == sizeof(u16))
return static_cast<T>(starlet->ReadHollywoodBus16(em_address));
if constexpr (sizeof(T) == sizeof(u32))
return static_cast<T>(starlet->ReadHollywoodRegister(em_address));
}
}
if (flag == XCheckTLBFlag::Read && (em_address & 0xF8000000) == 0x08000000)
{
if (em_address < 0x0c000000)
@@ -320,7 +340,7 @@ T MMU::ReadFromHardware(u32 em_address)
return 0;
}
template <XCheckTLBFlag flag, bool never_translate>
template <XCheckTLBFlag flag, bool never_translate, bool force_cache_inhibited>
void MMU::WriteToHardware(u32 em_address, const u32 data, const u32 size)
{
static_assert(flag == XCheckTLBFlag::NoException || flag == XCheckTLBFlag::Write);
@@ -336,13 +356,14 @@ void MMU::WriteToHardware(u32 em_address, const u32 data, const u32 size)
// Note that "word" means 32-bit, so paired singles or doubles might still be 32-bit aligned!
const u32 first_half_size = em_address_end_page - em_address;
const u32 second_half_size = size - first_half_size;
WriteToHardware<flag, never_translate>(em_address, std::rotr(data, second_half_size * 8),
first_half_size);
WriteToHardware<flag, never_translate>(em_address_end_page, data, second_half_size);
WriteToHardware<flag, never_translate, force_cache_inhibited>(
em_address, std::rotr(data, second_half_size * 8), first_half_size);
WriteToHardware<flag, never_translate, force_cache_inhibited>(em_address_end_page, data,
second_half_size);
return;
}
bool wi = false;
bool wi = force_cache_inhibited;
if (!never_translate && m_ppc_state.msr.DR)
{
@@ -354,7 +375,31 @@ void MMU::WriteToHardware(u32 em_address, const u32 data, const u32 size)
return;
}
em_address = translated_addr.address;
wi = translated_addr.wi;
wi |= translated_addr.wi;
}
if (em_address >= IOS::LLE::StarletMemory::SRAM_BASE &&
em_address <=
IOS::LLE::StarletMemory::SRAM_BASE + IOS::LLE::StarletMemory::SRAM_WINDOW_SIZE - size)
{
if (IOS::LLE::Starlet* const starlet = m_system.GetStarlet())
{
switch (size)
{
case 1:
starlet->WriteHollywoodBus8(em_address, static_cast<u8>(data));
return;
case 2:
starlet->WriteHollywoodBus16(em_address, static_cast<u16>(data));
return;
case 4:
starlet->WriteHollywoodRegister(em_address, data);
return;
default:
ASSERT(false);
return;
}
}
}
// Check for a gather pipe write (which are not implemented through the MMIO system).
@@ -452,8 +497,11 @@ void MMU::WriteToHardware(u32 em_address, const u32 data, const u32 size)
const u32 end_addr = Common::AlignUp(em_address + size, 8);
for (u32 addr = start_addr; addr != end_addr; addr += 8)
{
WriteToHardware<flag, true>(addr, rotated_data, 4);
WriteToHardware<flag, true>(addr + 4, rotated_data, 4);
// These are the physical bus transactions generated by the original cache-inhibited
// access. They must stay cache-inhibited after bypassing address translation; otherwise
// accurate D-cache emulation incorrectly turns them into write-back cache stores.
WriteToHardware<flag, true, true>(addr, rotated_data, 4);
WriteToHardware<flag, true, true>(addr + 4, rotated_data, 4);
}
return;
@@ -532,6 +580,25 @@ u32 MMU::Read_Opcode(u32 address)
TryReadInstResult MMU::TryReadInstruction(u32 address)
{
if (!m_ppc_state.msr.IR && m_system.GetWiiIPC().IsBroadwayInReset() &&
address == IOS::WiiIPC::BROADWAY_HOLD_PC)
{
// A held Broadway consumes no bus cycles on hardware. Dolphin still needs PPC instructions to
// advance CoreTiming, so execute a host-only branch-to-self outside emulated memory.
return TryReadInstResult{true, true, 0x48000000, address};
}
if (!m_ppc_state.msr.IR)
{
if (const auto* starlet = m_system.GetStarlet())
{
if (const std::optional<u32> instruction = starlet->TryReadBroadwayResetInstruction(address))
{
return TryReadInstResult{true, true, *instruction, address};
}
}
}
bool from_bat = true;
if (m_ppc_state.msr.IR)
{
+1 -1
View File
@@ -344,7 +344,7 @@ private:
template <XCheckTLBFlag flag, std::unsigned_integral T, bool never_translate = false>
T ReadFromHardware(u32 em_address);
template <XCheckTLBFlag flag, bool never_translate = false>
template <XCheckTLBFlag flag, bool never_translate = false, bool force_cache_inhibited = false>
void WriteToHardware(u32 em_address, u32 data, u32 size);
template <XCheckTLBFlag flag>
bool IsEffectiveRAMAddress(u32 address);
+12
View File
@@ -26,6 +26,7 @@
#include "Core/HW/SystemTimers.h"
#include "Core/HW/VideoInterface.h"
#include "Core/HW/WII_IPC.h"
#include "Core/IOS/Starlet/Starlet.h"
#include "Core/Movie.h"
#include "Core/PowerPC/Interpreter/Interpreter.h"
#include "Core/PowerPC/JitInterface.h"
@@ -69,6 +70,7 @@ struct System::Impl
bool m_audio_dump_started = false;
std::unique_ptr<IOS::HLE::EmulationKernel> m_ios;
std::unique_ptr<IOS::LLE::Starlet> m_starlet;
AudioInterface::AudioInterfaceManager m_audio_interface;
CoreTiming::CoreTimingManager m_core_timing;
@@ -162,6 +164,16 @@ void System::SetIOS(std::unique_ptr<IOS::HLE::EmulationKernel> ios)
m_impl->m_ios = std::move(ios);
}
IOS::LLE::Starlet* System::GetStarlet() const
{
return m_impl->m_starlet.get();
}
void System::SetStarlet(std::unique_ptr<IOS::LLE::Starlet> starlet)
{
m_impl->m_starlet = std::move(starlet);
}
AudioInterface::AudioInterfaceManager& System::GetAudioInterface() const
{
return m_impl->m_audio_interface;
+6
View File
@@ -62,6 +62,10 @@ namespace IOS::HLE
class EmulationKernel;
class USBScanner;
} // namespace IOS::HLE
namespace IOS::LLE
{
class Starlet;
}
namespace HSP
{
class HSPManager;
@@ -165,6 +169,8 @@ public:
IOS::HLE::EmulationKernel* GetIOS() const;
void SetIOS(std::unique_ptr<IOS::HLE::EmulationKernel> ios);
IOS::LLE::Starlet* GetStarlet() const;
void SetStarlet(std::unique_ptr<IOS::LLE::Starlet> starlet);
AudioInterface::AudioInterfaceManager& GetAudioInterface() const;
CPU::CPUManager& GetCPU() const;
@@ -14,8 +14,9 @@
#include "InputCommon/ControllerInterface/DInput/DInput.h"
// (lower would be more sensitive) user can lower sensitivity by setting range
// seems decent here ( at 8 ), I don't think anyone would need more sensitive than this
// and user can lower it much farther than they would want to with the range
// seems decent here ( at 8 ), I don't think anyone would need more sensitive
// than this and user can lower it much farther than they would want to with the
// range
#define MOUSE_AXIS_SENSITIVITY 8
// if input hasn't been received for this many ms, mouse input will be skipped
@@ -68,15 +69,15 @@ void InitKeyboardMouse(IDirectInput8* const idi8, HWND hwnd)
s_hwnd = hwnd;
// Mouse and keyboard are a combined device, to allow shift+click and stuff
// if that's dumb, I will make a VirtualDevice class that just uses ranges of inputs/outputs from
// other devices
// so there can be a separated Keyboard and mouse, as well as combined KeyboardMouse
// if that's dumb, I will make a VirtualDevice class that just uses ranges of
// inputs/outputs from other devices so there can be a separated Keyboard and
// mouse, as well as combined KeyboardMouse
LPDIRECTINPUTDEVICE8 kb_device = nullptr;
LPDIRECTINPUTDEVICE8 mo_device = nullptr;
// These are "virtual" system devices, so they are always there even if we have no physical
// mouse and keyboard plugged into the computer
// These are "virtual" system devices, so they are always there even if we
// have no physical mouse and keyboard plugged into the computer
if (SUCCEEDED(idi8->CreateDevice(GUID_SysKeyboard, &kb_device, nullptr)) &&
SUCCEEDED(kb_device->SetDataFormat(&c_dfDIKeyboard)) &&
SUCCEEDED(kb_device->SetCooperativeLevel(nullptr, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE)) &&
@@ -105,10 +106,12 @@ KeyboardMouse::~KeyboardMouse()
{
s_keyboard_mouse_exists = false;
// Independently of the order in which we do these, if we put a breakpoint on Unacquire() (or in
// any place in the call stack before this), when refreshing devices from the UI, on the second
// attempt, it will get stuck in an infinite (while) loop inside dinput8.dll. Given that it can't
// be otherwise be reproduced (not even with sleeps), we can just ignore the problem.
// Independently of the order in which we do these, if we put a breakpoint on
// Unacquire() (or in any place in the call stack before this), when
// refreshing devices from the UI, on the second attempt, it will get stuck in
// an infinite (while) loop inside dinput8.dll. Given that it can't be
// otherwise be reproduced (not even with sleeps), we can just ignore the
// problem.
// kb
m_kb_device->Unacquire();
@@ -152,7 +155,8 @@ KeyboardMouse::KeyboardMouse(const LPDIRECTINPUTDEVICE8 kb_device,
{
const LONG& ax = (&m_state_in.mouse.lX)[i];
// each axis gets a negative and a positive input instance associated with it
// each axis gets a negative and a positive input instance associated with
// it
AddInput(new Axis(i, ax, (2 == i) ? -1 : -MOUSE_AXIS_SENSITIVITY));
AddInput(new Axis(i, ax, -(2 == i) ? 1 : MOUSE_AXIS_SENSITIVITY));
}
@@ -171,7 +175,8 @@ KeyboardMouse::KeyboardMouse(const LPDIRECTINPUTDEVICE8 kb_device,
void KeyboardMouse::UpdateCursorInput()
{
// Get the size of the current window (in my case Rect.top and Rect.left was zero).
// Get the size of the current window (in my case Rect.top and Rect.left was
// zero).
RECT rect;
GetClientRect(s_hwnd, &rect);
@@ -193,10 +198,11 @@ void KeyboardMouse::UpdateCursorInput()
}
else if (Host_TASInputHasFocus())
{
// When a TAS Input window has focus and "Enable Controller Input" is checked most types of
// input should be read normally as if the render window had focus instead. The cursor is an
// exception, as otherwise using the mouse to set any control in the TAS Input window will also
// update the Wii IR value (or any other input controlled by the cursor).
// When a TAS Input window has focus and "Enable Controller Input" is
// checked most types of input should be read normally as if the render
// window had focus instead. The cursor is an exception, as otherwise using
// the mouse to set any control in the TAS Input window will also update the
// Wii IR value (or any other input controlled by the cursor).
return;
}
@@ -204,8 +210,8 @@ void KeyboardMouse::UpdateCursorInput()
{
GetCursorPos(&point);
// Get the cursor position relative to the upper left corner of the current window
// (separate or render to main)
// Get the cursor position relative to the upper left corner of the current
// window (separate or render to main)
ScreenToClient(s_hwnd, &point);
}
@@ -254,6 +260,17 @@ Core::DeviceRemoval KeyboardMouse::UpdateInput()
// copy over the buttons
std::copy_n(tmp_mouse.rgbButtons, std::size(tmp_mouse.rgbButtons), m_state_in.mouse.rgbButtons);
// DirectInput can miss synthetic and very short clicks even though Win32's
// asynchronous state still reports the button as held. Keeping the two
// sources additive makes mouse-backed Wii Remote mappings reliable without
// disturbing extra DirectInput buttons.
if (GetAsyncKeyState(VK_LBUTTON) & 0x8000)
m_state_in.mouse.rgbButtons[0] |= 0x80;
if (GetAsyncKeyState(VK_RBUTTON) & 0x8000)
m_state_in.mouse.rgbButtons[1] |= 0x80;
if (GetAsyncKeyState(VK_MBUTTON) & 0x8000)
m_state_in.mouse.rgbButtons[2] |= 0x80;
}
HRESULT kb_hr = m_kb_device->GetDeviceState(sizeof(m_state_in.keyboard), &m_state_in.keyboard);
+2
View File
@@ -14,6 +14,8 @@ add_dolphin_test(DSPAssemblyTest
add_dolphin_test(ESFormatsTest IOS/ES/FormatsTest.cpp)
add_dolphin_test(StarletARMCoreTest IOS/Starlet/ARMCoreTest.cpp)
add_dolphin_test(FileSystemTest IOS/FS/FileSystemTest.cpp)
add_dolphin_test(SkylandersTest IOS/USB/SkylandersTest.cpp)
@@ -0,0 +1,841 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
#include <vector>
#include <gtest/gtest.h>
#include "Common/CommonTypes.h"
#include "Core/Core.h"
#include "Core/HW/WII_IPC.h"
#include "Core/IOS/Starlet/ARMCore.h"
#include "Core/IOS/Starlet/StarletMemory.h"
#include "Core/System.h"
namespace IOS::LLE
{
namespace
{
// Native-width bus counters also verify that the CPU's generation-tagged
// caches bypass redundant traffic without changing architectural results.
class TestBus final : public ARMBus
{
public:
explicit TestBus(size_t size = 0x1000) : m_memory(size) {}
u8 Read8(u32 address) override
{
++m_read8_count;
const size_t offset = ToOffset(address);
EXPECT_LT(offset, m_memory.size());
return offset < m_memory.size() ? m_memory[offset] : 0;
}
u16 Read16(u32 address) override
{
++m_read16_count;
const size_t offset = ToOffset(address);
EXPECT_LT(offset + 1, m_memory.size());
if (offset + 1 >= m_memory.size())
return 0;
return static_cast<u16>((static_cast<u16>(m_memory[offset]) << 8) | m_memory[offset + 1]);
}
u32 Read32(u32 address) override
{
++m_read32_count;
const size_t offset = ToOffset(address);
EXPECT_LT(offset + 3, m_memory.size());
if (offset + 3 >= m_memory.size())
return 0;
return (static_cast<u32>(m_memory[offset]) << 24) |
(static_cast<u32>(m_memory[offset + 1]) << 16) |
(static_cast<u32>(m_memory[offset + 2]) << 8) | m_memory[offset + 3];
}
void Write8(u32 address, u8 value) override
{
const size_t offset = ToOffset(address);
ASSERT_LT(offset, m_memory.size());
m_memory[offset] = value;
}
void AdvanceCycles(u64 cycles) override
{
m_cycles += cycles;
++m_advance_calls;
}
bool IsIdlePollAddress(u32 address, u32 size) const override
{
const size_t offset = ToOffset(address);
return m_idle_poll_safe && size != 0 && offset <= m_memory.size() &&
size <= m_memory.size() - offset;
}
void SetIdlePollSafe(bool safe) { m_idle_poll_safe = safe; }
void WriteARM(u32 address, u32 instruction)
{
const size_t offset = ToOffset(address);
m_memory[offset] = static_cast<u8>(instruction >> 24);
m_memory[offset + 1] = static_cast<u8>(instruction >> 16);
m_memory[offset + 2] = static_cast<u8>(instruction >> 8);
m_memory[offset + 3] = static_cast<u8>(instruction);
}
void WriteThumb(u32 address, u16 instruction)
{
const size_t offset = ToOffset(address);
m_memory[offset] = static_cast<u8>(instruction >> 8);
m_memory[offset + 1] = static_cast<u8>(instruction);
}
u8 operator[](size_t address) const { return m_memory[address]; }
u64 GetCycles() const { return m_cycles; }
u64 GetAdvanceCalls() const { return m_advance_calls; }
u64 GetRead8Count() const { return m_read8_count; }
u64 GetRead16Count() const { return m_read16_count; }
u64 GetRead32Count() const { return m_read32_count; }
void ResetReadCounts()
{
m_read8_count = 0;
m_read16_count = 0;
m_read32_count = 0;
}
private:
static size_t ToOffset(u32 address)
{
return address >= 0xffff0000 ? address - 0xffff0000 : address;
}
std::vector<u8> m_memory;
u64 m_cycles = 0;
u64 m_advance_calls = 0;
u64 m_read8_count = 0;
u64 m_read16_count = 0;
u64 m_read32_count = 0;
bool m_idle_poll_safe = true;
};
TEST(WiiIPCCtrlRegister, ProducerBitsRemainLatchedUntilPeerAcknowledges)
{
IOS::CtrlRegister control;
control.ppc(0x08); // X2: completion acknowledgement.
control.ppc(0x01); // X1: next request must not clear X2.
EXPECT_EQ(control.arm() & 0x06, 0x06);
control.arm(0x06); // Starlet clears X1 and X2.
EXPECT_EQ(control.arm() & 0x06, 0x00);
control.arm(0x08); // Y2: request acknowledgement.
control.arm(0x01); // Y1: reply must not clear Y2.
EXPECT_EQ(control.ppc() & 0x06, 0x06);
control.ppc(0x06); // Broadway clears Y1 and Y2.
EXPECT_EQ(control.ppc() & 0x06, 0x00);
}
TEST(StarletTimer, ZeroDelayAlarmMatchesImmediatelyAndUsesIRQW1C)
{
constexpr u32 hardware_base = 0x0d800000;
constexpr u32 timer = hardware_base + 0x10;
constexpr u32 alarm = hardware_base + 0x14;
constexpr u32 arm_irq_flag = hardware_base + 0x38;
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));
};
memory.AdvanceCycles(405);
ASSERT_EQ(read_word(timer), 32u);
write_word(alarm, read_word(timer));
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, INT_CAUSE_TIMER);
// Reprogramming the comparator does not acknowledge an already latched timer
// interrupt.
write_word(alarm, 64);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, INT_CAUSE_TIMER);
write_word(arm_irq_flag, INT_CAUSE_TIMER);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, 0u);
memory.AdvanceCycles(404);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, 0u);
memory.AdvanceCycles(1);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, INT_CAUSE_TIMER);
}
TEST(StarletAHBPROT, OriginalIOSMaskIsPreservedForBroadway)
{
constexpr u32 ahbprot = 0x0d800064;
constexpr u32 broadway_access_mask = 0x80000dfe;
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));
};
// boot2 enables every permission. The System Menu's IOS clears Broadway's permissions, then
// IOS58 restores them when launching HBC because its TMD requests full hardware access.
EXPECT_EQ(read_word(ahbprot), 0xffffffffu);
write_word(ahbprot, 0xffffffffu & ~broadway_access_mask);
EXPECT_EQ(read_word(ahbprot), 0x7ffff201u);
write_word(ahbprot, read_word(ahbprot) | broadway_access_mask);
EXPECT_EQ(read_word(ahbprot), 0xffffffffu);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x64), 0xffffffffu);
}
TEST(StarletSDHC, EmptyControllerResetsAndStabilizesItsClock)
{
constexpr u32 base = 0x0d070000;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
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));
};
EXPECT_EQ(read_word(base + 0x40), 0x01603201u);
EXPECT_EQ(read_word(base + 0x24), 1U << 17);
write_word(base + 0x2c, 0x00000001); // Enable the internal clock.
EXPECT_EQ(read_word(base + 0x2c), 0x00000003u);
write_word(base + 0x2c, 0x01000003); // Reset-all in the reversed high byte.
EXPECT_EQ(read_word(base + 0x2c), 0x00000003u);
write_word(base + 0x34, 0xffffffff); // Enable normal and error statuses.
write_word(base + 0x38,
0xffffffff); // Route both status halves to the SD IRQ.
write_word(base + 0x08, 0x000001aa); // CMD8 argument.
write_word(base + 0x0c,
0x08020000); // CMD8 against an empty slot must time out.
EXPECT_EQ(read_word(base + 0x30), 0x00018000u);
write_word(base + 0x30, 0xffffffff); // Normal/error interrupt status W1C.
EXPECT_EQ(read_word(base + 0x30), 0u);
write_word(base + 0x0c,
0); // CMD0 is valid without a card and completes normally.
EXPECT_EQ(read_word(base + 0x30), 1u);
}
TEST(StarletEHCI, HollywoodResetImageAndPhyState)
{
constexpr u32 base = 0x0d040000;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
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));
};
EXPECT_EQ(read_word(base + 0x10), 0x00080000u);
EXPECT_EQ(read_word(base + 0x14), 0x00001000u);
write_word(base + 0xa4, 0x00004023);
write_word(base + 0xcc, 0x00000891);
write_word(base + 0x10, read_word(base + 0x10) | 2); // HCRESET self-clears.
EXPECT_EQ(read_word(base + 0x10), 0x00080000u);
EXPECT_EQ(read_word(base + 0x14), 0x00001000u);
EXPECT_EQ(read_word(base + 0xa4), 0x00004023u);
EXPECT_EQ(read_word(base + 0xcc), 0x00000891u);
}
TEST(StarletUSB, IOSReloadReturnsHostControllersToResetState)
{
constexpr u32 ehci = 0x0d040000;
constexpr u32 ohci0 = 0x0d050000;
constexpr u32 ohci1 = 0x0d060000;
constexpr u32 usb_force_reset = 0x0d800088;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
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));
};
write_word(ehci + 0x10, 0x00080001); // Run the high-speed controller.
write_word(ohci0 + 0x04, 0x00000283); // IOS80 leaves both OHCIs operational.
write_word(ohci1 + 0x04, 0x00000283);
ASSERT_EQ(read_word(ohci0 + 0x04), 0x00000283u);
ASSERT_EQ(read_word(ohci1 + 0x04), 0x00000283u);
// The IOS kernel asserts Hollywood's USB force-reset lines before loading
// the replacement IOS host-controller modules.
write_word(usb_force_reset, 0x000000fe);
EXPECT_EQ(read_word(ehci + 0x10), 0x00080000u);
EXPECT_EQ(read_word(ehci + 0x14), 0x00001000u);
EXPECT_EQ(read_word(ohci0), 0x10u);
EXPECT_EQ(read_word(ohci1), 0x10u);
EXPECT_EQ(read_word(ohci0 + 0x04), 0u);
EXPECT_EQ(read_word(ohci1 + 0x04), 0u);
}
TEST(StarletOHCI, InternalBluetoothPortUsesHardwareRootHubSemantics)
{
constexpr u32 ohci0 = 0x0d050000;
constexpr u32 ohci1 = 0x0d060000;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
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));
};
const auto read_le_word = [&memory](u32 address) {
return static_cast<u32>(memory.Read8(address)) |
static_cast<u32>(memory.Read8(address + 1)) << 8 |
static_cast<u32>(memory.Read8(address + 2)) << 16 |
static_cast<u32>(memory.Read8(address + 3)) << 24;
};
const auto write_le_word = [&memory](u32 address, u32 value) {
memory.Write8(address, static_cast<u8>(value));
memory.Write8(address + 1, static_cast<u8>(value >> 8));
memory.Write8(address + 2, static_cast<u8>(value >> 16));
memory.Write8(address + 3, static_cast<u8>(value >> 24));
};
EXPECT_EQ(read_word(ohci0), 0x10u);
EXPECT_EQ(read_word(ohci1), 0x10u);
EXPECT_EQ(read_word(ohci0 + 0x48) & 0xff, 2u);
EXPECT_EQ(read_word(ohci1 + 0x48) & 0xff, 1u);
EXPECT_EQ(read_word(ohci0 + 0x48) >> 24, 2u);
EXPECT_EQ(read_word(ohci1 + 0x48) >> 24, 2u);
EXPECT_EQ(read_word(ohci0 + 0x4c), 0u);
EXPECT_EQ(read_word(ohci1 + 0x4c),
2u); // Port 1 is the non-removable Bluetooth daughterboard.
EXPECT_EQ(read_word(ohci0 + 0x54), 0u);
EXPECT_EQ(read_word(ohci1 + 0x54), 0x00010101u);
write_word(ohci1 + 0x10, 0x80000040); // Master + root-hub status-change IRQ.
EXPECT_EQ(read_word(ohci1 + 0x10), 0x80000040u);
write_word(ohci1 + 0x04,
2U << 6); // USB operational, so its 1 ms frame clock runs.
write_word(ohci1 + 0x54, 0x00010000); // Clear connection-status-change.
EXPECT_EQ(read_word(ohci1 + 0x54), 0x00000101u);
write_word(ohci1 + 0x0c, 0x40); // HcInterruptStatus is W1C.
EXPECT_EQ(read_word(ohci1 + 0x0c) & 0x40, 0u);
write_word(ohci1 + 0x54, 1U << 4); // Reset the attached Bluetooth port.
EXPECT_EQ(read_word(ohci1 + 0x54),
0x00000111u); // Reset signaling is active until the next poll.
memory.AdvanceCycles(243000);
EXPECT_EQ(read_word(ohci1 + 0x54), 0x00000111u);
memory.AdvanceCycles(243000);
EXPECT_EQ(read_word(ohci1 + 0x54),
0x00100103u); // Enabled + reset complete change.
EXPECT_EQ(read_word(ohci1 + 0x0c) & 0x40, 0x40u);
memory.AdvanceCycles(243000);
EXPECT_EQ(read_word(ohci1 + 0x3c), 3u);
write_word(ohci1 + 0x08, 1); // Host-controller reset self-clears.
EXPECT_EQ(read_word(ohci1 + 0x08), 0u);
EXPECT_EQ(read_word(ohci1 + 0x34), 0x27782edfu);
EXPECT_EQ(read_word(ohci1 + 0x54), 0x00010101u);
// Queue a complete GET_DESCRIPTOR(Device) control transfer using the same
// little-endian ED/TD/HCCA structures that the original IOS OHCI module
// builds in Starlet memory.
constexpr u32 hcca = StarletMemory::SRAM_BASE;
constexpr u32 endpoint = hcca + 0x100;
constexpr u32 setup_td = hcca + 0x200;
constexpr u32 data_td = hcca + 0x210;
constexpr u32 status_td = hcca + 0x220;
constexpr u32 tail_td = hcca + 0x230;
constexpr u32 setup_buffer = hcca + 0x300;
constexpr u32 data_buffer = hcca + 0x340;
const std::array<u8, 8> get_device_descriptor = {0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x40, 0x00};
for (size_t i = 0; i < get_device_descriptor.size(); ++i)
memory.Write8(setup_buffer + static_cast<u32>(i), get_device_descriptor[i]);
write_word(ohci1 + 0x04,
2U << 6); // Restart the frame clock after controller reset.
write_word(ohci1 + 0x54, 1U << 4); // Reset the device to USB address zero.
memory.AdvanceCycles(2 * 243000);
write_le_word(endpoint, 64U << 16); // Address 0, endpoint 0, max packet 64.
write_le_word(endpoint + 4, tail_td);
write_le_word(endpoint + 8, setup_td);
write_le_word(endpoint + 12, 0);
write_le_word(setup_td, 0xf0000000); // SETUP, condition code NotAccessed.
write_le_word(setup_td + 4, setup_buffer);
write_le_word(setup_td + 8, data_td);
write_le_word(setup_td + 12, setup_buffer + 7);
write_le_word(data_td, 0xf0140000); // Rounding + IN.
write_le_word(data_td + 4, data_buffer);
write_le_word(data_td + 8, status_td);
write_le_word(data_td + 12, data_buffer + 63);
write_le_word(status_td, 0xf0080000); // Zero-length OUT status stage.
write_le_word(status_td + 4, 0);
write_le_word(status_td + 8, tail_td);
write_le_word(status_td + 12, 0);
write_word(ohci1 + 0x18, hcca);
write_word(ohci1 + 0x20, endpoint);
write_word(ohci1 + 0x04, (2U << 6) | (1U << 4));
EXPECT_EQ(read_le_word(endpoint + 8) & ~0xfU, tail_td);
EXPECT_EQ(read_le_word(setup_td) >> 28, 0u);
EXPECT_EQ(read_le_word(data_td) >> 28, 0u);
EXPECT_EQ(read_le_word(status_td) >> 28, 0u);
EXPECT_EQ(memory.Read8(data_buffer), 0x12u);
EXPECT_EQ(memory.Read8(data_buffer + 1), 0x01u);
EXPECT_EQ(memory.Read8(data_buffer + 8), 0x7eu);
EXPECT_EQ(memory.Read8(data_buffer + 9), 0x05u);
EXPECT_EQ(read_le_word(hcca + 0x84), status_td);
EXPECT_EQ(read_word(ohci1 + 0x0c) & (1U << 1), 1U << 1);
// A busy IOS can leave WDH pending while more transfers finish. Those TDs
// must remain in HcDoneHead instead of overwriting the unconsumed
// HCCA.DoneHead list.
constexpr u32 setup_td2 = hcca + 0x400;
constexpr u32 data_td2 = hcca + 0x410;
constexpr u32 status_td2 = hcca + 0x420;
constexpr u32 tail_td2 = hcca + 0x430;
constexpr u32 setup_buffer2 = hcca + 0x500;
constexpr u32 data_buffer2 = hcca + 0x540;
for (size_t i = 0; i < get_device_descriptor.size(); ++i)
memory.Write8(setup_buffer2 + static_cast<u32>(i), get_device_descriptor[i]);
write_le_word(endpoint + 4, tail_td2);
write_le_word(endpoint + 8, setup_td2);
write_le_word(setup_td2, 0xf0000000);
write_le_word(setup_td2 + 4, setup_buffer2);
write_le_word(setup_td2 + 8, data_td2);
write_le_word(setup_td2 + 12, setup_buffer2 + 7);
write_le_word(data_td2, 0xf0140000);
write_le_word(data_td2 + 4, data_buffer2);
write_le_word(data_td2 + 8, status_td2);
write_le_word(data_td2 + 12, data_buffer2 + 63);
write_le_word(status_td2, 0xf0080000);
write_le_word(status_td2 + 4, 0);
write_le_word(status_td2 + 8, tail_td2);
write_le_word(status_td2 + 12, 0);
write_word(ohci1 + 0x08, 1U << 1); // Ring the control-list doorbell.
EXPECT_EQ(read_le_word(endpoint + 8) & ~0xfU, tail_td2);
EXPECT_EQ(read_le_word(hcca + 0x84), status_td);
EXPECT_EQ(read_word(ohci1 + 0x30) & ~0xfU, status_td2);
write_word(ohci1 + 0x0c,
1U << 1); // IOS consumes and acknowledges the first done list.
memory.AdvanceCycles(243000);
EXPECT_EQ(read_le_word(hcca + 0x84), status_td2);
EXPECT_EQ(read_word(ohci1 + 0x0c) & (1U << 1), 1U << 1);
}
TEST(StarletARMCore, DataProcessingAndConditions)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe3a0102a); // mov r1, #42
bus.WriteARM(0x04, 0xe2812001); // add r2, r1, #1
bus.WriteARM(0x08, 0xe352002b); // cmp r2, #43
bus.WriteARM(0x0c, 0x13a03001); // movne r3, #1 (must be skipped)
bus.WriteARM(0x10, 0x03a03007); // moveq r3, #7
core.Run(5);
EXPECT_EQ(core.GetRegister(1), 42u);
EXPECT_EQ(core.GetRegister(2), 43u);
EXPECT_EQ(core.GetRegister(3), 7u);
EXPECT_NE(core.GetCPSR() & ARMCore::CPSR_Z, 0u);
EXPECT_EQ(core.GetRegister(15), 0x14u);
}
TEST(StarletARMCore, BigEndianLoadStore)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe5801004); // str r1, [r0, #4]
bus.WriteARM(0x04, 0xe5902004); // ldr r2, [r0, #4]
core.SetRegister(0, 0x100);
core.SetRegister(1, 0x11223344);
core.Run(2);
EXPECT_EQ(bus[0x104], 0x11);
EXPECT_EQ(bus[0x105], 0x22);
EXPECT_EQ(bus[0x106], 0x33);
EXPECT_EQ(bus[0x107], 0x44);
EXPECT_EQ(core.GetRegister(2), 0x11223344u);
}
TEST(StarletARMCore, VectorLiteralLoadMatchesBootROMShape)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe59ff018); // ldr pc, [pc, #24]
bus.WriteARM(0x20, 0x00000040); // reset handler address
bus.WriteARM(0x40, 0xe3a0102a); // mov r1, #42
core.Step();
EXPECT_EQ(core.GetRegister(15), 0x40u);
core.Step();
EXPECT_EQ(core.GetRegister(1), 42u);
}
TEST(StarletARMCore, LoadsToPCInterworkIntoThumb)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe59ff000); // ldr pc, [pc]
bus.WriteARM(0x08, 0x00000101); // Thumb entry address
bus.WriteThumb(0x100, 0x212a); // mov r1, #42
bus.WriteARM(0x200, 0xe8b08000); // ldmia r0!, {pc}
bus.WriteARM(0x300, 0x00000101); // Thumb entry address
core.Step();
EXPECT_NE(core.GetCPSR() & ARMCore::CPSR_T, 0u);
EXPECT_EQ(core.GetRegister(15), 0x100u);
core.Step();
EXPECT_EQ(core.GetRegister(1), 42u);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor));
core.SetRegister(0, 0x300);
core.SetRegister(15, 0x200);
core.Step();
EXPECT_NE(core.GetCPSR() & ARMCore::CPSR_T, 0u);
EXPECT_EQ(core.GetRegister(15), 0x100u);
}
TEST(StarletARMCore, BlockTransfersCanAccessTheUserBank)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe8de6001); // ldmia lr, {r0, sp, lr}^
bus.WriteARM(0x300, 0x11223344); // user r0
bus.WriteARM(0x304, 0x55667788); // user sp
bus.WriteARM(0x308, 0x99aabbcc); // user lr
core.SetCPSR(static_cast<u32>(ARMCore::Mode::System));
core.SetRegister(13, 0x11111111);
core.SetRegister(14, 0x22222222);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor));
core.SetRegister(14, 0x300);
core.Step();
EXPECT_EQ(core.GetRegister(0), 0x11223344u);
EXPECT_EQ(core.GetRegister(14), 0x300u);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::System));
EXPECT_EQ(core.GetRegister(13), 0x55667788u);
EXPECT_EQ(core.GetRegister(14), 0x99aabbccu);
}
TEST(StarletARMCore, BranchLinkAndExchange)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xeb000002); // bl 0x10
bus.WriteARM(0x10, 0xe12fff1e); // bx lr
core.Step();
EXPECT_EQ(core.GetRegister(14), 4u);
EXPECT_EQ(core.GetRegister(15), 0x10u);
core.Step();
EXPECT_EQ(core.GetRegister(15), 4u);
}
TEST(StarletARMCore, IRQUsesBankedRegistersAndSPSR)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0xffff0018, 0xe25ef004); // subs pc, lr, #4
core.SetCPSR(static_cast<u32>(ARMCore::Mode::System));
core.SetRegister(13, 0x11110000);
core.SetRegister(15, 0x100);
core.SetIRQLine(true);
core.Step();
EXPECT_EQ(core.GetMode(), ARMCore::Mode::IRQ);
EXPECT_EQ(core.GetRegister(15), 0xffff0018u);
EXPECT_EQ(core.GetRegister(14), 0x104u);
core.SetIRQLine(false);
core.SetRegister(13, 0x22220000);
core.Step();
EXPECT_EQ(core.GetMode(), ARMCore::Mode::System);
EXPECT_EQ(core.GetRegister(15), 0x100u);
EXPECT_EQ(core.GetRegister(13), 0x11110000u);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::IRQ) | ARMCore::CPSR_I);
EXPECT_EQ(core.GetRegister(13), 0x22220000u);
}
TEST(StarletARMCore, CP15Identification)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xee100f10); // mrc p15, 0, r0, c0, c0, 0
core.Step();
EXPECT_EQ(core.GetRegister(0), 0x41069265u);
}
TEST(StarletARMCore, CP15TestAndCleanReportsUnmodeledCacheClean)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xee17ff7a); // mrc p15, 0, pc, c7, c10, 3
core.Step();
EXPECT_NE(core.GetCPSR() & ARMCore::CPSR_Z, 0u);
EXPECT_EQ(core.GetCPSR() & (ARMCore::CPSR_N | ARMCore::CPSR_C | ARMCore::CPSR_V), 0u);
EXPECT_EQ(core.GetRegister(15), 4u);
}
TEST(StarletARMCore, CP15InstructionCacheMaintenanceInvalidatesCachedCode)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe3a01001); // mov r1, #1
bus.WriteARM(0x04,
0xee070f15); // mcr p15, 0, r0, c7, c5, 0 (invalidate I-cache)
core.Step();
ASSERT_EQ(core.GetRegister(1), 1u);
bus.WriteARM(0x00,
0xe3a01002); // The cached instruction remains visible until CP15 c7.
core.SetRegister(15, 0);
core.Step();
EXPECT_EQ(core.GetRegister(1), 1u);
core.SetRegister(15, 4);
core.Step();
core.SetRegister(15, 0);
core.Step();
EXPECT_EQ(core.GetRegister(1), 2u);
}
TEST(StarletARMCore, WaitForInterruptFastForwardsAndWakesOnMaskedIRQ)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00,
0xee070f90); // mcr p15, 0, r0, c7, c0, 4 (wait for interrupt)
bus.WriteARM(0x04, 0xe2811001); // add r1, r1, #1
core.Step();
ASSERT_TRUE(core.IsWaitingForInterrupt());
EXPECT_EQ(core.GetRegister(15), 4u);
EXPECT_EQ(core.GetExecutedInstructions(), 1u);
EXPECT_EQ(core.RunCycles(10000), 10000u);
EXPECT_TRUE(core.IsWaitingForInterrupt());
EXPECT_EQ(core.GetRegister(15), 4u);
EXPECT_EQ(core.GetExecutedInstructions(), 1u);
EXPECT_EQ(bus.GetCycles(), 10001u);
// An asserted line wakes ARM926 even while CPSR.I masks exception entry.
core.SetIRQLine(true);
EXPECT_FALSE(core.IsWaitingForInterrupt());
core.Step();
EXPECT_EQ(core.GetRegister(1), 1u);
EXPECT_EQ(core.GetRegister(15), 8u);
}
TEST(StarletARMCore, WaitForInterruptWakesIntoUnmaskedIRQVector)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xee070f90); // mcr p15, 0, r0, c7, c0, 4
core.SetCPSR(static_cast<u32>(ARMCore::Mode::System));
core.Step();
ASSERT_TRUE(core.IsWaitingForInterrupt());
core.SetIRQLine(true);
EXPECT_EQ(core.Step(), 3);
EXPECT_FALSE(core.IsWaitingForInterrupt());
EXPECT_EQ(core.GetMode(), ARMCore::Mode::IRQ);
EXPECT_EQ(core.GetRegister(15), 0xffff0018u);
}
TEST(StarletARMCore, ThumbExecutionAndConditions)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe12fff10); // bx r0
bus.WriteThumb(0x100, 0x212a); // mov r1, #42
bus.WriteThumb(0x102, 0x3101); // add r1, #1
bus.WriteThumb(0x104, 0x292b); // cmp r1, #43
bus.WriteThumb(0x106, 0xd100); // bne (must be skipped)
bus.WriteThumb(0x108, 0x2207); // mov r2, #7
core.SetRegister(0, 0x101);
core.Run(6);
EXPECT_NE(core.GetCPSR() & ARMCore::CPSR_T, 0u);
EXPECT_EQ(core.GetRegister(1), 43u);
EXPECT_EQ(core.GetRegister(2), 7u);
EXPECT_EQ(core.GetRegister(15), 0x10au);
}
TEST(StarletARMCore, MultiplyAndLongMultiply)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe0000291); // mul r0, r1, r2
bus.WriteARM(0x04, 0xe0834291); // umull r4, r3, r1, r2
core.SetRegister(1, 0x10000);
core.SetRegister(2, 0x10001);
core.Run(2);
EXPECT_EQ(core.GetRegister(0), 0x00010000u);
EXPECT_EQ(core.GetRegister(4), 0x00010000u);
EXPECT_EQ(core.GetRegister(3), 1u);
}
TEST(StarletARMCore, RunCyclesStopsAtBudget)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe2800001); // add r0, r0, #1
bus.WriteARM(0x04, 0xeafffffd); // b 0x0
EXPECT_EQ(core.RunCycles(8), 8u);
EXPECT_EQ(core.GetExecutedInstructions(), 8u);
EXPECT_EQ(core.GetRegister(0), 4u);
EXPECT_EQ(bus.GetCycles(), 8u);
EXPECT_EQ(bus.GetAdvanceCalls(), 1u);
EXPECT_EQ(bus.GetRead32Count(),
2u); // Both words stay resident across loop iterations.
}
TEST(StarletARMCore, ThumbMemoryPollingFastForwardsSafeRAMUntilItChanges)
{
TestBus bus;
ARMCore core(bus);
bus.WriteThumb(0x00, 0x6823); // ldr r3, [r4]
bus.WriteThumb(0x02, 0x2b00); // cmp r3, #0
bus.WriteThumb(0x04, 0xd0fc); // beq 0x00
core.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor) | ARMCore::CPSR_T);
core.SetRegister(4, 0x100);
EXPECT_EQ(core.RunCycles(10000), 10000u);
EXPECT_EQ(core.GetExecutedInstructions(), 3u);
EXPECT_EQ(bus.GetCycles(), 10000u);
EXPECT_TRUE(core.IsWaitingForExternalEvent());
bus.Write32(0x100, 1);
EXPECT_EQ(core.RunCycles(2), 2u);
EXPECT_EQ(core.GetExecutedInstructions(), 5u);
EXPECT_EQ(core.GetRegister(3), 1u);
EXPECT_FALSE(core.IsWaitingForExternalEvent());
}
TEST(StarletARMCore, ThumbMemoryPollingDoesNotSkipMMIO)
{
TestBus bus;
ARMCore core(bus);
bus.SetIdlePollSafe(false);
bus.WriteThumb(0x00, 0x6823); // ldr r3, [r4]
bus.WriteThumb(0x02, 0x2b00); // cmp r3, #0
bus.WriteThumb(0x04, 0xd0fc); // beq 0x00
core.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor) | ARMCore::CPSR_T);
core.SetRegister(4, 0x100);
EXPECT_EQ(core.RunCycles(12), 12u);
EXPECT_EQ(core.GetExecutedInstructions(), 12u);
EXPECT_FALSE(core.IsWaitingForExternalEvent());
}
TEST(StarletARMCore, ExceptionEntryAdvancesDeviceCycles)
{
TestBus bus;
ARMCore core(bus);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::System));
core.SetIRQLine(true);
EXPECT_EQ(core.Step(), 3);
EXPECT_EQ(bus.GetCycles(), 3u);
}
TEST(StarletARMCore, MMUSectionTranslation)
{
TestBus bus(0x10000);
ARMCore core(bus);
bus.WriteARM(0x0000, 0xe3a0002a); // mov r0, #42
bus.WriteARM(0x0004, 0xe3a01007); // mov r1, #7
bus.WriteARM(0x6000,
0x00000c02); // VA 0x80000000 section -> PA 0, full access
core.GetCP15State().translation_table_base = 0x4000;
core.GetCP15State().domain_access_control = 3;
core.GetCP15State().control |= 1;
core.SetRegister(15, 0x80000000);
bus.ResetReadCounts();
core.Run(2);
EXPECT_EQ(core.GetRegister(0), 42u);
EXPECT_EQ(core.GetRegister(1), 7u);
EXPECT_EQ(core.GetRegister(15), 0x80000008u);
EXPECT_EQ(bus.GetRead8Count(), 0u);
EXPECT_EQ(bus.GetRead16Count(), 0u);
// The first fetch walks the section table; the second hits the 1 KiB
// translation cache.
EXPECT_EQ(bus.GetRead32Count(), 3u);
}
} // namespace
} // namespace IOS::LLE
+45
View File
@@ -0,0 +1,45 @@
param(
[Parameter(Mandatory = $true)] [int] $ProcessId,
[Parameter(Mandatory = $true)] [UInt64] $Address,
[Parameter(Mandatory = $true)] [int] $Length,
[Parameter(Mandatory = $true)] [string] $OutputPath
)
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public static class ProcessMemoryDumpNative
{
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(uint access, bool inheritHandle, int processId);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool ReadProcessMemory(IntPtr process, UIntPtr address, byte[] buffer,
UIntPtr size, out UIntPtr bytesRead);
[DllImport("kernel32.dll")]
public static extern bool CloseHandle(IntPtr handle);
}
'@
$handle = [ProcessMemoryDumpNative]::OpenProcess(0x410, $false, $ProcessId)
if ($handle -eq [IntPtr]::Zero) {
throw "OpenProcess failed: $([Runtime.InteropServices.Marshal]::GetLastWin32Error())"
}
try {
$buffer = [byte[]]::new($Length)
$bytesRead = [UIntPtr]::Zero
if (-not [ProcessMemoryDumpNative]::ReadProcessMemory(
$handle, [UIntPtr]::new($Address), $buffer, [UIntPtr]::new([UInt64]$Length), [ref]$bytesRead)) {
throw "ReadProcessMemory failed: $([Runtime.InteropServices.Marshal]::GetLastWin32Error())"
}
if ($bytesRead.ToUInt64() -ne [UInt64]$Length) {
throw "Short read: requested $Length bytes, got $($bytesRead.ToUInt64())"
}
[IO.File]::WriteAllBytes((Join-Path (Get-Location) $OutputPath), $buffer)
}
finally {
[void][ProcessMemoryDumpNative]::CloseHandle($handle)
}
+160
View File
@@ -0,0 +1,160 @@
param(
[Parameter(Mandatory = $true)]
[int]$ProcessId,
[Parameter(Mandatory = $true)]
[string]$OutputPath
)
$source = @'
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
public static class StarletSramScanner
{
[StructLayout(LayoutKind.Sequential)]
private struct MEMORY_BASIC_INFORMATION
{
public IntPtr BaseAddress;
public IntPtr AllocationBase;
public uint AllocationProtect;
public ushort PartitionId;
public UIntPtr RegionSize;
public uint State;
public uint Protect;
public uint Type;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(uint access, bool inherit, int processId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr handle);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern UIntPtr VirtualQueryEx(IntPtr process, IntPtr address,
out MEMORY_BASIC_INFORMATION information,
UIntPtr length);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool ReadProcessMemory(IntPtr process, IntPtr address, byte[] buffer,
UIntPtr size, out UIntPtr bytesRead);
private const uint PROCESS_VM_READ = 0x0010;
private const uint PROCESS_QUERY_INFORMATION = 0x0400;
private const uint MEM_COMMIT = 0x1000;
private const uint PAGE_NOACCESS = 0x01;
private const uint PAGE_GUARD = 0x100;
private const int SRAM_SIZE = 0x18000;
private const int SIGNATURE_OFFSET = 0x540;
private static readonly byte[] Signature =
{
0xe5, 0x93, 0x00, 0x00, 0xe1, 0x51, 0x08, 0x20, 0x0a, 0xff, 0xff, 0xfc
};
private static int Find(byte[] haystack, int count)
{
for (int i = 0; i <= count - Signature.Length; ++i)
{
int j = 0;
while (j < Signature.Length && haystack[i + j] == Signature[j])
++j;
if (j == Signature.Length)
return i;
}
return -1;
}
public static long Dump(int processId, string outputPath)
{
IntPtr process = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, processId);
if (process == IntPtr.Zero)
throw new Win32Exception(Marshal.GetLastWin32Error(), "OpenProcess failed");
try
{
ulong address = 0x10000;
ulong maximum = 0x00007fffffff0000UL;
int mbiSize = Marshal.SizeOf<MEMORY_BASIC_INFORMATION>();
while (address < maximum)
{
MEMORY_BASIC_INFORMATION mbi;
UIntPtr queried = VirtualQueryEx(process, new IntPtr(unchecked((long)address)),
out mbi, new UIntPtr((uint)mbiSize));
if (queried == UIntPtr.Zero)
break;
ulong baseAddress = unchecked((ulong)mbi.BaseAddress.ToInt64());
ulong regionSize = mbi.RegionSize.ToUInt64();
if (regionSize == 0)
break;
bool readable = mbi.State == MEM_COMMIT && (mbi.Protect & PAGE_NOACCESS) == 0 &&
(mbi.Protect & PAGE_GUARD) == 0;
if (readable)
{
const int chunkSize = 4 * 1024 * 1024;
ulong offset = 0;
while (offset < regionSize)
{
int requested = (int)Math.Min((ulong)chunkSize, regionSize - offset);
byte[] chunk = new byte[requested];
UIntPtr bytesRead;
if (ReadProcessMemory(process,
new IntPtr(unchecked((long)(baseAddress + offset))),
chunk, new UIntPtr((uint)requested), out bytesRead))
{
int hit = Find(chunk, checked((int)bytesRead.ToUInt64()));
if (hit >= 0)
{
ulong signatureAddress = baseAddress + offset + (uint)hit;
if (signatureAddress < SIGNATURE_OFFSET)
break;
ulong sramAddress = signatureAddress - SIGNATURE_OFFSET;
byte[] sram = new byte[SRAM_SIZE];
UIntPtr sramRead;
if (ReadProcessMemory(process,
new IntPtr(unchecked((long)sramAddress)), sram,
new UIntPtr(SRAM_SIZE), out sramRead) &&
sramRead.ToUInt64() == SRAM_SIZE && FindAt(sram, SIGNATURE_OFFSET))
{
File.WriteAllBytes(outputPath, sram);
return unchecked((long)sramAddress);
}
}
}
offset += (ulong)requested;
}
}
address = baseAddress + regionSize;
if (address <= baseAddress)
break;
}
}
finally
{
CloseHandle(process);
}
throw new InvalidOperationException("Starlet SRAM signature was not found");
}
private static bool FindAt(byte[] bytes, int offset)
{
if (offset < 0 || offset + Signature.Length > bytes.Length)
return false;
for (int i = 0; i < Signature.Length; ++i)
{
if (bytes[offset + i] != Signature[i])
return false;
}
return true;
}
}
'@
Add-Type -TypeDefinition $source -Language CSharp
$resolvedOutput = [System.IO.Path]::GetFullPath($OutputPath)
$address = [StarletSramScanner]::Dump($ProcessId, $resolvedOutput)
"Starlet SRAM dumped from host address 0x{0:x16} to {1}" -f $address, $resolvedOutput
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""Generate a LetterBomb tree directly in Dolphin's virtual SD folder."""
from __future__ import annotations
import argparse
import hashlib
import hmac
import shutil
import zipfile
from datetime import datetime, timedelta
from pathlib import Path, PurePosixPath
REPO_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_SOURCE = REPO_ROOT / ".starlet_check" / "letterbomb-web" / "public"
def parse_mac(text: str) -> bytes:
compact = text.replace(":", "").replace("-", "")
if len(compact) != 12:
raise argparse.ArgumentTypeError("MAC must contain exactly 12 hexadecimal digits")
try:
return bytes.fromhex(compact)
except ValueError as error:
raise argparse.ArgumentTypeError("MAC contains a non-hexadecimal character") from error
def safe_extract(archive: zipfile.ZipFile, destination: Path) -> None:
for member in archive.infolist():
relative = PurePosixPath(member.filename)
if relative.is_absolute() or ".." in relative.parts:
raise RuntimeError(f"Unsafe archive member: {member.filename!r}")
target = destination.joinpath(*relative.parts)
if member.is_dir():
target.mkdir(parents=True, exist_ok=True)
continue
target.parent.mkdir(parents=True, exist_ok=True)
with archive.open(member) as source, target.open("wb") as output:
shutil.copyfileobj(source, output)
def generate(template: bytes, mac: bytes, message_time: datetime) -> tuple[bytes, Path]:
payload = bytearray(template)
key = hashlib.sha1(mac + b"uyy").digest()
payload[8:16] = key[:8]
payload[176:196] = bytes(20)
epoch = datetime(2000, 1, 1)
timestamp = int((message_time - epoch).total_seconds())
payload[124:128] = timestamp.to_bytes(4, "big")
payload[128:138] = f"{timestamp:010d}".encode("ascii")
payload[176:196] = hmac.new(key[8:], payload, hashlib.sha1).digest()
relative_path = Path(
"private",
"wii",
"title",
"HAEA",
key[:4].hex().upper(),
key[4:8].hex().upper(),
f"{message_time.year:04d}",
f"{message_time.month - 1:02d}",
f"{message_time.day:02d}",
f"{message_time.hour:02d}",
f"{message_time.minute:02d}",
"HABA_#1",
"txt",
f"{timestamp:08X}.000",
)
return bytes(payload), relative_path
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--mac", required=True, type=parse_mac)
parser.add_argument("--region", required=True, choices=("E", "U", "J", "K"))
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE)
parser.add_argument(
"--date",
help="message date as YYYY-MM-DD; defaults to yesterday in local Wii time",
)
args = parser.parse_args()
source = args.source.resolve()
output = args.output.resolve()
template_path = source / f"template{args.region}.bin"
bundle_path = source / "hackmii.zip"
if not template_path.is_file() or not bundle_path.is_file():
raise RuntimeError(f"LetterBomb assets are missing from {source}")
if args.date:
message_time = datetime.strptime(args.date, "%Y-%m-%d")
else:
message_time = datetime.now().replace(second=0, microsecond=0) - timedelta(days=1)
payload, relative_path = generate(template_path.read_bytes(), args.mac, message_time)
output.mkdir(parents=True, exist_ok=True)
target = output / relative_path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(payload)
with zipfile.ZipFile(bundle_path) as archive:
safe_extract(archive, output)
mac_text = ":".join(f"{byte:02X}" for byte in args.mac)
(output / "LETTERBOMB-INFO.txt").write_text(
"LetterBomb virtual Wii test card\n"
f"System Menu: 4.3{args.region}\n"
f"Emulated Wi-Fi MAC: {mac_text}\n"
f"Message date: {message_time:%Y-%m-%d %H:%M}\n"
f"Message file: {relative_path.as_posix()}\n"
"Close Dolphin before changing files in this folder.\n",
encoding="utf-8",
)
print(f"LetterBomb generated at {target}")
print(f"Bundled HackMii Installer extracted to {output}")
if __name__ == "__main__":
main()
+82
View File
@@ -0,0 +1,82 @@
param(
[Parameter(Mandatory = $true)]
[int]$ProcessId,
[int64]$MinimumSize = 0x1000000
)
$source = @'
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
public static class ProcessMemoryRegions
{
[StructLayout(LayoutKind.Sequential)]
public struct Region
{
public IntPtr BaseAddress;
public IntPtr AllocationBase;
public uint AllocationProtect;
public ushort PartitionId;
public UIntPtr RegionSize;
public uint State;
public uint Protect;
public uint Type;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(uint access, bool inherit, int processId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr handle);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern UIntPtr VirtualQueryEx(IntPtr process, IntPtr address,
out Region information, UIntPtr length);
public static Region[] List(int processId, long minimumSize)
{
IntPtr process = OpenProcess(0x0400, false, processId);
if (process == IntPtr.Zero)
throw new Win32Exception(Marshal.GetLastWin32Error(), "OpenProcess failed");
var result = new List<Region>();
try
{
ulong address = 0x10000;
const ulong maximum = 0x00007fffffff0000UL;
int size = Marshal.SizeOf<Region>();
while (address < maximum)
{
Region region;
if (VirtualQueryEx(process, new IntPtr(unchecked((long)address)), out region,
new UIntPtr((uint)size)) == UIntPtr.Zero)
break;
ulong baseAddress = unchecked((ulong)region.BaseAddress.ToInt64());
ulong regionSize = region.RegionSize.ToUInt64();
if (regionSize == 0)
break;
if (region.State == 0x1000 && regionSize >= (ulong)minimumSize)
result.Add(region);
address = baseAddress + regionSize;
if (address <= baseAddress)
break;
}
}
finally
{
CloseHandle(process);
}
return result.ToArray();
}
}
'@
Add-Type -TypeDefinition $source -Language CSharp
[ProcessMemoryRegions]::List($ProcessId, $MinimumSize) | ForEach-Object {
[pscustomobject]@{
Base = '0x{0:x16}' -f [uint64]$_.BaseAddress.ToInt64()
AllocationBase = '0x{0:x16}' -f [uint64]$_.AllocationBase.ToInt64()
Size = '0x{0:x}' -f $_.RegionSize.ToUInt64()
Protect = '0x{0:x}' -f $_.Protect
Type = '0x{0:x}' -f $_.Type
}
}
+53
View File
@@ -0,0 +1,53 @@
param(
[Parameter(Mandatory = $true)]
[int]$ProcessId,
[Parameter(Mandatory = $true)]
[uint64]$Address,
[int]$Length = 64
)
$source = @'
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
public static class ProcessMemoryReader
{
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(uint access, bool inherit, int processId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr handle);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool ReadProcessMemory(IntPtr process, IntPtr address, byte[] buffer,
UIntPtr size, out UIntPtr bytesRead);
public static byte[] Read(int processId, ulong address, int length)
{
IntPtr process = OpenProcess(0x0010 | 0x0400, false, processId);
if (process == IntPtr.Zero)
throw new Win32Exception(Marshal.GetLastWin32Error(), "OpenProcess failed");
try
{
byte[] bytes = new byte[length];
UIntPtr read;
if (!ReadProcessMemory(process, new IntPtr(unchecked((long)address)), bytes,
new UIntPtr((uint)length), out read) ||
read.ToUInt64() != (ulong)length)
throw new Win32Exception(Marshal.GetLastWin32Error(), "ReadProcessMemory failed");
return bytes;
}
finally
{
CloseHandle(process);
}
}
}
'@
Add-Type -TypeDefinition $source -Language CSharp
$bytes = [ProcessMemoryReader]::Read($ProcessId, $Address, $Length)
for ($offset = 0; $offset -lt $bytes.Length; $offset += 16) {
$count = [Math]::Min(16, $bytes.Length - $offset)
$hex = ($bytes[$offset..($offset + $count - 1)] | ForEach-Object { '{0:x2}' -f $_ }) -join ' '
'0x{0:x16}: {1}' -f ($Address + [uint64]$offset), $hex
}
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
import argparse
import ctypes
import pathlib
import sys
REPOSITORY_ROOT = pathlib.Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPOSITORY_ROOT / "capstone_local"))
from capstone import CS_ARCH_ARM, CS_ARCH_PPC, CS_MODE_32, CS_MODE_ARM, CS_MODE_BIG_ENDIAN, Cs
PROCESS_VM_READ = 0x0010
PROCESS_QUERY_INFORMATION = 0x0400
def read_process_memory(process_id: int, address: int, length: int) -> bytes:
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
kernel32.OpenProcess.argtypes = [ctypes.c_uint32, ctypes.c_bool, ctypes.c_uint32]
kernel32.OpenProcess.restype = ctypes.c_void_p
kernel32.ReadProcessMemory.argtypes = [
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_size_t,
ctypes.POINTER(ctypes.c_size_t),
]
kernel32.ReadProcessMemory.restype = ctypes.c_bool
kernel32.CloseHandle.argtypes = [ctypes.c_void_p]
kernel32.CloseHandle.restype = ctypes.c_bool
process = kernel32.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, process_id)
if not process:
raise ctypes.WinError(ctypes.get_last_error())
try:
buffer = ctypes.create_string_buffer(length)
bytes_read = ctypes.c_size_t()
if not kernel32.ReadProcessMemory(
process, ctypes.c_void_p(address), buffer, length, ctypes.byref(bytes_read)
):
raise ctypes.WinError(ctypes.get_last_error())
if bytes_read.value != length:
raise RuntimeError(f"short read: requested {length} bytes, got {bytes_read.value}")
return buffer.raw
finally:
kernel32.CloseHandle(process)
def main() -> None:
parser = argparse.ArgumentParser(
description="Disassemble big-endian 32-bit PowerPC code from a live Windows process."
)
parser.add_argument("process_id", type=int)
parser.add_argument("host_address", type=lambda value: int(value, 0))
parser.add_argument("guest_address", type=lambda value: int(value, 0))
parser.add_argument("length", type=lambda value: int(value, 0))
parser.add_argument("--arch", choices=("ppc", "arm"), default="ppc")
args = parser.parse_args()
code = read_process_memory(args.process_id, args.host_address, args.length)
if args.arch == "arm":
disassembler = Cs(CS_ARCH_ARM, CS_MODE_ARM | CS_MODE_BIG_ENDIAN)
else:
disassembler = Cs(CS_ARCH_PPC, CS_MODE_32 | CS_MODE_BIG_ENDIAN)
for instruction in disassembler.disasm(code, args.guest_address):
operands = f" {instruction.op_str}" if instruction.op_str else ""
print(f"{instruction.address:08x}: {instruction.bytes.hex(' '):11} "
f"{instruction.mnemonic}{operands}")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+374
View File
@@ -0,0 +1,374 @@
# Wii IOS LLE / Starlet
This experimental mode replaces Dolphin's IOS HLE kernel with an emulated ARM926EJ-S (Starlet)
and executes the console-specific Wii firmware from local dumps. It is disabled by default. The
validated path now reaches the original IOS title launch, `IOS_StartPPC`, the EXI Broadway boot
stub, release of both Broadway reset lines, and execution of the IOS-loaded PowerPC title image in
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.
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.
## Configuration
Add the following values to Dolphin's main configuration:
```ini
[Core]
WiiIOSLLE = True
CPUThread = True
[General]
WiiIOSLLEDumpPath = C:/absolute/path/to/dumps
```
The selected directory must contain the three matching files from one console:
| File | Exact size | Purpose |
|---|---:|---|
| `boot0.bin` | `0x2000` bytes | Immutable Starlet reset ROM |
| `keys.bin` | `0x400` bytes | BootMii-format OTP/key dump |
| `nand.bin` | `0x21000000` or `0x21000400` bytes | Raw NAND with spare/ECC, optionally followed by the BootMii keys footer |
For a `0x21000400`-byte BootMii dump, Dolphin compares the OTP and SEEPROM portions of the appended
footer with the separate `keys.bin`. A mismatch is rejected before any firmware executes. The
footer is validation metadata and is never exposed as NAND pages.
If validation fails, Dolphin reports the reason and stops the LLE boot. It never falls back to IOS
HLE while the mode is selected, because silently switching kernels would invalidate hardware and
exploit testing. The dump path and file data are not logged.
To request a normal NAND boot in Dolphin NoGUI, select the System Menu title ID. In LLE mode this is
a boot-chain selector, not a direct HLE title launch:
```text
DolphinNoGUI --nand_title 0000000100000002
```
The host does not look up or inject the System Menu TMD/content for this path. boot2 and the original
IOS select, authenticate, load, and start the title from the raw NAND. Other direct NAND-title,
WAD, and DOL/ELF launch requests remain rejected in LLE mode.
## Execution path
```text
Dolphin CoreTiming (Broadway clock domain, 729 MHz)
|
+-- active: 12,288 Broadway cycles --> 4,096 Starlet cycles (243 MHz)
+-- WFI idle: 72,900 Broadway cycles --> 24,300 Starlet cycles
|
v
ARMv5TE interpreter
+ software TLB/I-cache
|
+-------------------------------+------------------------------+
| | |
boot0 ROM SRAM / MEM1 / MEM2 Hollywood MMIO
0xffff0000 shared memory NAND, AES, SHA, OTP, timer,
IRQ and IPC mailboxes
|
v
original IOS `/dev/*`
(when IOS is reached)
```
On reset, Dolphin parks Broadway on a host-only branch while CoreTiming continues to run; no word in
MEM1 is overwritten to create that loop. Starlet owns the normal boot sequence. Writes to
`HW_RESETS` reproduce the console's SRESET/HRESET transitions. The 64-byte EXI boot buffer written by IOS
at `0x0d806840` is exposed to Broadway at its real high reset vector, `0xfff00100`, once
`DIFLAGS_BOOT_CODE` enables the mapping. Releasing both reset lines resets the Broadway
register/cache state and executes those original instructions. The observed stub sets `SRR0` to
`0x3400` and reaches it through `rfi`; the code there establishes the BAT/MSR state and branches to
the IOS-loaded title entry at `0x81000000`. There is no host-side jump to either address. Direct
disc BS2, DOL/ELF, WAD, and arbitrary NAND-title launch paths are deliberately not mixed with this
mode because they depend on IOS HLE. Selecting the System Menu is the one exception: it starts the
raw NAND boot chain and does not directly launch the selected title.
PPC-to-Starlet commands use the existing Wii IPC registers. In LLE mode, an X1 request asserts the
Starlet IPC interrupt instead of calling `IOS::HLE::Kernel::EnqueueIPCRequest`. ARM replies and
acknowledgements update the same mailbox/control state and can interrupt Broadway. Consequently,
`/dev/fs`, `/dev/es`, and the other resource managers are intended to be created and executed by
the original IOS image—not reimplemented as LLE-specific HLE services.
The X1/X2 and Y1/Y2 producer bits are hardware latches rather than ordinary writable fields. A
producer write of zero leaves its pending bit unchanged; only the peer's write-one-to-clear bit
acknowledges it. This matters for back-to-back traffic: libogc can acknowledge one IOS reply with
X2 and immediately submit the next request with X1 before Starlet is scheduled again.
## Implemented hardware
- ARM and Thumb instruction interpretation needed by the observed boot, including banked CPU
modes, user-bank block transfers, SPSRs, high Starlet exception vectors, IRQ/FIQ entry,
ARM/Thumb interworking, ARMv5TE branches/multiplies, and CP15 registers.
- Initial ARM926 MMU translation for sections, coarse/fine page tables, and FCSE process IDs. A
generation-tagged 1 KiB software TLB caches resolved translations and is invalidated by the
original CP15 control/table/process-ID/TLB-maintenance writes.
- A physically tagged 64 KiB instruction cache avoids repeated bus fetches in hot IOS loops. CP15
`c7` maintenance invalidates it in O(1) through generation tags. ARM926 wait-for-interrupt stops
instruction issue, fast-forwards device time, and wakes on an asserted IRQ/FIQ even when masked.
- Native aligned 16-/32-bit reads translate a virtual address once and directly access boot ROM,
SRAM, MEM1, or MEM2. Direct writes remain enabled for MEM1/MEM2, while SRAM writes deliberately
retain byte-lane dispatch: treating a multi-byte SRAM store as one host write corrupted the
IOS-to-System-Menu handoff and produced the Wii fatal-error screen. Active execution advances
timer, NAND, OHCI, and Wiimote clocks once per 4,096-cycle scheduler slice instead of once per
interpreted instruction; external IRQ delivery remains bounded to about 16.9 microseconds and
WFI polling to 100 microseconds.
- Big-endian Starlet address space, 96 KiB of physical SRAM (64 KiB bank A plus 32 KiB bank B)
exposed through the hardware's unusual 128 KiB windows, plus shared MEM1/MEM2 access.
- Raw NAND reads, chip identification/status, Wii ECC generation, ECC-enabled page programming
(including the calculated-ECC DMA side buffer and random spare input), and 64-page block erase.
Programming obeys the NAND 1-to-0 bit rule.
- NAND and SEEPROM writes are session-local copy-on-write data. They participate in save states and
are discarded on a fresh emulated reset; the dump and `keys.bin` remain opened/read as immutable
source material.
- AES-CBC encrypt/decrypt DMA with the hardware key/IV FIFOs, SHA-1 compression, and the OTP
boot1-hash reads used by boot0.
- The 19.2 MHz Hollywood timer/alarm comparator, including immediate matches when programmed to
the current counter value, separate write-one-to-clear acknowledgement, completion causes, and
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.
- 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.
- 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,
and simple block DMA to Dolphin's copy-on-write-configured `WiiSD.raw` image. The implemented
commands cover identification/selection, OCR/CID/CSD/SCR, block-size setup, status, and
single/multiple-block reads and writes against Dolphin's copy-on-write SD image.
- The Starlet DI window at `0x0d806000` is forwarded to Dolphin's existing drive-interface MMIO
implementation (the `0x0d006000` hardware block), including byte-lane accesses and Hollywood DI
interrupt source `0x200`. This lets the original `/dev/di` resource manager service the System
Menu's post-health-screen cover/status commands instead of timing out and creating
`/shared2/test2/dvderror.dat`.
- Initial OpenHCI 1.0 host-controller path at `0x0d050000`/`0x0d060000`: controller reset and
functional states, interrupt status/enable/disable, 1 ms frame clock, HCCA frame/done-head
writeback, control/bulk/periodic list traversal, little-endian ED/TD DMA, and root-hub
power/connect/reset/change semantics routed through Hollywood IRQs 5 and 6. OHCI0 currently has
two empty external ports; OHCI1 exposes the internal `057e:0305` Bluetooth module and its USB
device/configuration/interface/endpoint descriptors. The emulated reset descriptor advertises a
nonzero two-unit power-on-to-power-good delay (4 ms in OpenHCI units), which the original driver
consumes through the original IOS timer service before scanning the hub. Standard endpoint-zero
enumeration is implemented without synthesizing an IOS IPC reply. The internal BCM2045 model
handles the HCI command/event subset used while IOS80 initializes Bluetooth, plus ACL transport
to Dolphin's existing remote-side L2CAP/HID implementation. It reuses trusted remote identities
already present in the dumped `BT.DINF` without logging their addresses. If no usable identity
exists, a session-only fallback pairing is installed by decrypting, updating, HMAC-authenticating,
ECC-correcting, and re-encrypting the affected SFFS cluster in the raw NAND COW overlay. The
final LLE-side remote devices are rebound to Dolphin's configured input sources after their NAND
identities are installed. ACL delivery waits for the connection-complete event, but unrelated
HCI events no longer block the second HID channel and deadlock the L2CAP handshake. The source
NAND remains unchanged.
- Hollywood `HW_USBFRCRST` reset-domain behavior across an IOS reload. When the original kernel
asserts a USB force-reset line, EHCI and both OHCI controllers return to their hardware reset
images before the replacement IOS drivers inspect them. This prevents operational state left by
IOS80 from making IOS58 reject OHCI0 before it can issue its own host-controller reset.
- The internal Wi-Fi SDIO host at `0x0d080000`/`0x0d880000`, including SDIO CMD5/7/52/53,
byte/word PIO and DMA transfers, CCCR/FBR/CIS data, clock and interrupt state, and the BCM4318
function-1 backplane aperture. The minimal Sonics core set exposes ChipCommon, D11, PCI and SDIO
IDs, the D11 bus-width aliases, G-PHY/radio identity, and the Wii board/MAC/antenna CIS values
required by the original Broadcom WL module. IOS80 completes hardware attach and registers its
original `/dev/wl`; no host network connection or packet-level 802.11 model is provided yet.
- The EXI Broadway boot buffer and its `0xfff00100` reset-vector mapping. The actual IOS-written PPC
stub is decoded and executed by Dolphin's normal Broadway core.
- Save-state serialization of ARM, SRAM, device, IPC, and scheduler state.
## Validated milestone
An isolated boot probe using the local, mutually matching dumps has executed this original chain:
1. `boot0` read 47 raw NAND pages, checked their ECC, decrypted boot1 with the emulated AES engine,
hashed it with the emulated SHA engine, compared the result with the console OTP value, and
transferred control to boot1.
2. boot1 trained the emulated memory controller, read and authenticated boot2, ran its ELF loader,
swapped the SRAM banks, removed the boot0 overlay, and entered the boot2/IOS image at
`0xffff0000`.
3. The IOS kernel changed from ARM to Thumb through an interworking `LDR pc`, built its translation
table at physical `0x13850000`, set its domain access register, and enabled the MMU with control
value `0x1087`.
4. The kernel entered its physical MEM2 code, mapped and ran module code at virtual
`0x20000000`/`0x20100000`, dispatched the firmware's intentional undefined-instruction system
calls through the high SRAM vectors, and performed thousands of real thread context switches.
5. boot2 created and encrypted `/tmp/launch.sys`, renamed it to `/sys/launch.sys`, and invoked the
original `ios_boot` syscall. The reloaded IOS80 mounted the newly written, HMAC- and ECC-valid
SFFS superblock and registered its original `/`, `/dev/boot2`, `/dev/es`, `/dev/di`, `/dev/stm/*`,
`/dev/usb/oh0`, `/dev/usb/oh1`, and `/dev/flash` resource managers.
6. IOS80 reopened `/sys/launch.sys`, loaded the requested title data, deleted the consumed launch
record, updated `/sys/space.sys`, and called `IOS_StartPPC`. The syscall completed successfully,
IOS populated the 64-byte EXI boot buffer, and both Broadway reset lines were released.
7. Before releasing reset, IOS had populated both the low-memory bootstrap at physical `0x3400`
and the title image backing virtual `0x81000000`. Mnemonic-only inspection confirmed a coherent
reset-stub chain (`rfi` to `0x3400`, BAT/MSR setup, then branch to the title entry) without
printing firmware bytes.
8. A full MSVC-built `DolphinNoGUI` run, using an isolated user directory and the System Menu boot
selector, executed the same chain through Dolphin's normal CoreTiming scheduler. It crossed the
1, 10, and 100 million ARM-instruction milestones, released Broadway through the EXI vector,
executed the title's BAT/HID setup, then continued through PowerPC system calls, external
interrupts, and periodic decrementer exceptions. The HLE Wii filesystem setup is explicitly
skipped when Starlet is active, preventing it from dereferencing or mutating an IOS HLE kernel
that intentionally does not exist in this mode.
9. The original PowerPC IPC client closed its initial 32 descriptor slots, opened the IOS80
resource managers `/dev/es`, `/dev/stm/immediate`, `/dev/stm/eventhook`, `/dev/fs`, and
`/dev/di`, and received ARM-generated acknowledgements and replies for sustained back-to-back
requests. The original filesystem stack read `SYSCONF`, `setting.txt`, `state.dat`,
`NANDBOOTINFO`, `loader.ini`, and a multi-megabyte System Menu content from the raw NAND. The
subsequently loaded PowerPC code reopened its IOS services and continued through IPC,
decrementer, external-interrupt, syscall, and floating-point-unavailable exception handling.
10. Complete runs with both an empty slot and a configured 128 MiB `WiiSD.raw` image reproduced the
same boot, multi-megabyte NAND load, second-stage IPC, and persistent PowerPC execution without
a regression. Synthetic tests validate reset/clock/status behavior and empty-slot command
timeout, while the inserted-card run validates discovery stability. The Menu had not yet
reached a `/dev/sdio/slot0` data request before blocking on USB, so original-IOS command-response
ordering and DMA remain to be proven end to end. That run observed the original title opening
`/dev/usb/oh1/57e/305`. A synthetic OHCI regression now proves endpoint-zero descriptor DMA,
done-head writeback, and WDH delivery; original-IOS enumeration is being validated separately
because root-hub discovery and device protocol timing remain incomplete.
11. An instrumented IOS80 run initialized both original OHCI drivers, read the emulated
`POTPGT=2` value, powered OHCI1 port 1, and waited 4 ms through the original IOS timer queue
before performing its initial empty-port scan. After initialization, the delayed internal
device attachment asserted Hollywood source 6. The original interrupt handler read and
acknowledged `RHSC`, sent its private root-hub-change message, and the original resource-manager
task dispatched it. That task observed `CSC|CCS`, acknowledged the connection change, initiated
a port reset, waited through its original 2 ms timer path, then observed `CCS|PES|PRSC` and
acknowledged reset completion. The same original driver then completed USB descriptor
enumeration and initialized the BCM2045 through HCI command/event traffic.
12. The IOS80 Broadcom WL module enumerated the emulated BCM4318 Sonics cores, passed its 32-/16-bit
D11 aperture tests, identified the G-PHY and BCM2050 radio, consumed the Wii CIS board and
antenna data, and registered `/dev/wl`. This allowed the original KD/NCD services to answer the
System Menu's `/dev/net/kd/request` and `/dev/net/kd/time` calls.
13. A headless Direct3D 11 run loaded the remaining System Menu resources through the original ES,
FS, DI and network resource managers, initialized VI/GX, and captured consecutive rendered
frames. The validated frame is the official French “Attention Santé et sécurité” screen,
proving visible System Menu startup after the original IOS boot and Broadway release.
14. A subsequent run selected two trusted Wii Remote identities from the raw NAND `BT.DINF`, then
let the original IOS80 WUD and OHCI1 drivers perform the BCM2045 initialization and incoming
connection path. IOS accepted the first L2CAP connection response instead of returning
`L2CAP_SECURITY_BLOCK`, completed the HID control and interrupt channels, issued its normal
LED/report-mode/extension setup traffic, and received sustained `A1 37` input reports. No IOS
IPC resource manager or `/dev/usb/oh1` request was synthesized by the host.
15. A controller-driven run sent the health-screen `A` report through those original IOS80 HID
channels, completed the original `/dev/di` status path, and rendered the populated French Wii
Menu channel grid. The previous fatal-error path and `dvderror.dat` creation did not recur.
16. Performance work replaced byte-at-a-time instruction/MMU reads, per-instruction device clocking,
and repeated page-table/code fetches with safe native-width reads and MEM1/MEM2 writes, sliced
device advancement, a software TLB, and an instruction cache. SRAM writes keep their original
byte-lane path after an end-to-end regression test showed that the direct host-width shortcut
reached Broadway quickly but made the System Menu display its fatal-error screen. This benchmark
covers the ARM-active boot path.
17. Interactive profiling measured the populated Wii Menu at 13.00 FPS and 18% speed before the
final scheduler optimization. The dominant path was the original IOS Thumb scheduler loop
`LDR [Rn] -> CMP #0 -> BEQ`, which polls a shared RAM word while no thread is runnable. The ARM
core now recognizes that general side-effect-free shape only when the polled physical address
is RAM or SRAM, advances device time without replaying millions of identical instructions, and
rechecks the word or any unmasked interrupt every 100 microseconds of Wii time. MMIO polling is
never skipped. A clean Direct3D 11 run then held 100% emulation speed and 59.91 FPS on the same
populated Menu screen.
18. Launching the installed Homebrew Channel from that Menu exercised a real IOS reload from IOS80
to IOS58. The original kernel asserted `HW_USBFRCRST`; the replacement IOS registered and
initialized `/dev/usb/oh0`, `/dev/usb/ehc`, `/dev/usb/usb`, and `/dev/usb/ven`, then released
Broadway into the channel. HBC rendered its normal interface at 59.94 FPS without modifying its
executable or bypassing the reload.
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
transfers, latched bidirectional IPC control bits, empty-slot SDHCI reset/clock/status behavior,
timer equal/future comparator matches with independent IRQ write-one-to-clear acknowledgement,
OHCI power-good/root-hub state, and a complete three-TD USB device-descriptor transaction. Targeted
development probes additionally exercised NAND read/program/erase and program-time ECC, SEEPROM
COW behavior, and AES-CBC encrypt/decrypt FIFO semantics. A separate differential harness compared
330,000 randomized
aligned ARM and Thumb cases against an ARM926-configured Unicorn 2.1.4 instance without a mismatch;
ARMv5 rotated unaligned word loads were tested separately because modern Unicorn models different
semantics.
This proves the original
`boot0 -> boot1 -> boot2 -> IOS kernel/modules -> title launch -> IOS_StartPPC -> Broadway release`
path is executing rather than being bypassed by Dolphin. At 120 million interpreted ARM/Thumb
instructions the probe observed no undefined ARM instruction outside IOS's syscall encoding, only
the three pre-existing bad-ECC pages in the source dump, a valid EXI reset vector, and
`HW_RESETS == 0xffffffff`.
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 and
a rendered Homebrew Channel. 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
The next firmware stages need substantially more hardware fidelity:
- ARM926 translation faults/aborts, domain and page permissions, replacement-policy fidelity, and
exact cache-maintenance timing. Address translation and functional TLB/I-cache invalidation exist,
but these protection and cycle-level details remain incomplete.
- Remaining memory-controller/AHB coherency behavior needed after the Broadway handoff and by
shared-memory DMA devices.
- NAND command latency, bad-block/wear behavior, error injection, and less common command variants.
The common IOS read/program/erase/ECC path is COW, but it is not yet a cycle-accurate flash model.
- Remaining GPIO inputs/interrupt behavior and external devices connected to those pins.
- DI, remaining SD command/response fidelity and insertion events, USB EHCI, packet-level Wi-Fi,
remaining Bluetooth/Wii Remote extensions, multi-controller and reconnection behavior, and their
DMA/interrupt timing. External SD block DMA, Wi-Fi SDIO/D11 attach, OHCI ED/TD DMA, USB
enumeration, Bluetooth HCI/ACL, and one paired remote's L2CAP/HID input path are implemented.
- Remaining IOS reload/reset edge cases, device timing, and scheduler accuracy needed by
timing-sensitive original exploits.
- Performance: the Menu's original IOS idle path now runs at full speed through safe RAM/SRAM poll
fast-forwarding. ARM-heavy boot and transient driver workloads still use the interpreter and can
take longer than real hardware; a block cache or ARM JIT remains the next performance frontier
for sustained workloads that do not enter the scheduler's idle loop.
Until those items are implemented, this is an end-to-end experimental LLE implementation rather
than a drop-in replacement for Dolphin's mature IOS HLE mode.
## Public implementation references
- [fail0verflow/mini](https://github.com/fail0verflow/mini) for public Starlet hardware constants,
SRAM swapping, MMU setup, and the boot2 ELF-loader shape.
- [InvoxiPlayGames/Uniicorn-Wii](https://github.com/InvoxiPlayGames/Uniicorn-Wii) for an independent
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 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.
- [WiiBrew Hollywood IRQs](https://www.wiibrew.org/wiki/Hollywood/IRQs) for the Starlet IRQ/FIQ
routing registers and interrupt-source assignments.
- [Linux Wii device tree](https://github.com/torvalds/linux/blob/master/arch/powerpc/boot/dts/wii.dts)
and [WiiBrew USB host controller](https://wiibrew.org/wiki/Hardware/USB_Host_Controller) for the
two OHCI register windows and Hollywood interrupt assignments.
- [OpenHCI 1.0a](https://www.cs.usfca.edu/~cruse/cs698s10/hcir1_0a.pdf) for controller, HCCA,
endpoint/transfer descriptor, root-hub, power-good-delay, and interrupt semantics.
- [WiiBrew IOS syscalls](https://www.wiibrew.org/wiki/IOS/Syscalls) and
[IOS kernel](https://www.wiibrew.org/wiki/IOS/Kernel) for the public timer/alarm and message-queue
interfaces used while correlating the original driver's control flow.
- [ARM926EJ-S Technical Reference Manual](https://developer.arm.com/documentation/ddi0198/latest/)
for ARMv5TE, CP15, banked-register, exception, and interworking semantics.
## Code map
| Area | Files |
|---|---|
| ARM CPU | `Core/IOS/Starlet/ARMCore.{h,cpp}` |
| Starlet scheduler/lifetime | `Core/IOS/Starlet/Starlet.{h,cpp}` |
| Address space and devices | `Core/IOS/Starlet/StarletMemory.{h,cpp}` |
| Bluetooth pairing and Wii Remote HID | `Core/IOS/USB/Bluetooth/{BTBase,WiimoteDevice}.{h,cpp}` |
| IPC and Broadway reset | `Core/HW/WII_IPC.{h,cpp}` |
| Mode selection/lifetime | `Core/HW/HW.cpp`, `Core/System.{h,cpp}` |
| Boot-path separation | `Core/Boot/Boot.cpp`, `Core/ConfigManager.cpp`, `Core/Core.cpp` |
| Configuration | `Core/Config/MainSettings.{h,cpp}` |
| Starlet CPU/device unit tests | `UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp` |
## Safe development rules
1. Treat all dumps as private console material and never print keys or ROM/NAND bytes in logs.
2. Keep `nand.bin` read-only; implement writes only in a separate copy-on-write overlay.
3. Test new devices first with synthetic data, then with address/PC traces that reveal no secrets.
4. Do not call IOS HLE from LLE code. Missing hardware should fail visibly instead of silently
changing the firmware behavior, especially while testing exploits.