From a77c156bc8aca299bc1e05c6e856198cfd601b24 Mon Sep 17 00:00:00 2001 From: Yaya48 Date: Tue, 25 Aug 2026 17:11:49 +0200 Subject: [PATCH] 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. --- .gitignore | 18 + Manage-Wii-SD.ps1 | 43 + Open-Virtual-SD.cmd | 2 + Run-Wii-IOS-LLE.cmd | 2 + Run-Wii-IOS-LLE.ps1 | 40 + Source/Core/Core/Boot/Boot.cpp | 36 + Source/Core/Core/CMakeLists.txt | 6 + Source/Core/Core/Config/MainSettings.cpp | 2 + Source/Core/Core/Config/MainSettings.h | 2 + Source/Core/Core/ConfigManager.cpp | 20 + Source/Core/Core/Core.cpp | 6 +- Source/Core/Core/HW/DVD/DVDInterface.cpp | 3 + Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp | 38 +- Source/Core/Core/HW/HW.cpp | 46 +- Source/Core/Core/HW/Memmap.cpp | 1 + Source/Core/Core/HW/SystemTimers.cpp | 2 +- Source/Core/Core/HW/WII_IPC.cpp | 285 +- Source/Core/Core/HW/WII_IPC.h | 37 +- Source/Core/Core/HW/Wiimote.cpp | 7 + Source/Core/Core/IOS/Starlet/ARMCore.cpp | 1946 ++++++++ Source/Core/Core/IOS/Starlet/ARMCore.h | 260 + Source/Core/Core/IOS/Starlet/Starlet.cpp | 153 + Source/Core/Core/IOS/Starlet/Starlet.h | 84 + .../Core/Core/IOS/Starlet/StarletMemory.cpp | 4297 +++++++++++++++++ Source/Core/Core/IOS/Starlet/StarletMemory.h | 273 ++ Source/Core/Core/IOS/USB/Bluetooth/BTBase.cpp | 60 +- Source/Core/Core/IOS/USB/Bluetooth/BTBase.h | 4 + Source/Core/Core/IOS/USB/Bluetooth/BTEmu.cpp | 27 +- Source/Core/Core/IOS/USB/Bluetooth/BTEmu.h | 27 +- .../Core/IOS/USB/Bluetooth/WiimoteDevice.cpp | 49 +- .../Core/IOS/USB/Bluetooth/WiimoteDevice.h | 23 +- Source/Core/Core/PowerPC/MMU.cpp | 83 +- Source/Core/Core/PowerPC/MMU.h | 2 +- Source/Core/Core/System.cpp | 12 + Source/Core/Core/System.h | 6 + .../DInput/DInputKeyboardMouse.cpp | 55 +- Source/UnitTests/Core/CMakeLists.txt | 2 + .../Core/IOS/Starlet/ARMCoreTest.cpp | 841 ++++ Tools/Dump-Process-Memory.ps1 | 45 + Tools/Dump-Starlet-SRAM.ps1 | 160 + Tools/Generate-LetterBomb.py | 121 + Tools/List-Process-MemoryRegions.ps1 | 82 + Tools/Read-Process-Memory.ps1 | 53 + Tools/disassemble-process-memory.py | 75 + docs/GPU_Emulation.md | 1004 ++++ docs/Wii_IOS_LLE.md | 374 ++ 46 files changed, 10556 insertions(+), 158 deletions(-) create mode 100644 Manage-Wii-SD.ps1 create mode 100644 Open-Virtual-SD.cmd create mode 100644 Run-Wii-IOS-LLE.cmd create mode 100644 Run-Wii-IOS-LLE.ps1 create mode 100644 Source/Core/Core/IOS/Starlet/ARMCore.cpp create mode 100644 Source/Core/Core/IOS/Starlet/ARMCore.h create mode 100644 Source/Core/Core/IOS/Starlet/Starlet.cpp create mode 100644 Source/Core/Core/IOS/Starlet/Starlet.h create mode 100644 Source/Core/Core/IOS/Starlet/StarletMemory.cpp create mode 100644 Source/Core/Core/IOS/Starlet/StarletMemory.h create mode 100644 Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp create mode 100644 Tools/Dump-Process-Memory.ps1 create mode 100644 Tools/Dump-Starlet-SRAM.ps1 create mode 100644 Tools/Generate-LetterBomb.py create mode 100644 Tools/List-Process-MemoryRegions.ps1 create mode 100644 Tools/Read-Process-Memory.ps1 create mode 100644 Tools/disassemble-process-memory.py create mode 100644 docs/GPU_Emulation.md create mode 100644 docs/Wii_IOS_LLE.md diff --git a/.gitignore b/.gitignore index 7e16a1442f..5af4dd6882 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Manage-Wii-SD.ps1 b/Manage-Wii-SD.ps1 new file mode 100644 index 0000000000..c80c52c116 --- /dev/null +++ b/Manage-Wii-SD.ps1 @@ -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.' + } +} diff --git a/Open-Virtual-SD.cmd b/Open-Virtual-SD.cmd new file mode 100644 index 0000000000..8a3538efdd --- /dev/null +++ b/Open-Virtual-SD.cmd @@ -0,0 +1,2 @@ +@echo off +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Manage-Wii-SD.ps1" Open diff --git a/Run-Wii-IOS-LLE.cmd b/Run-Wii-IOS-LLE.cmd new file mode 100644 index 0000000000..e333ac3bba --- /dev/null +++ b/Run-Wii-IOS-LLE.cmd @@ -0,0 +1,2 @@ +@echo off +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Run-Wii-IOS-LLE.ps1" diff --git a/Run-Wii-IOS-LLE.ps1 b/Run-Wii-IOS-LLE.ps1 new file mode 100644 index 0000000000..9925801118 --- /dev/null +++ b/Run-Wii-IOS-LLE.ps1 @@ -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 +} diff --git a/Source/Core/Core/Boot/Boot.cpp b/Source/Core/Core/Boot/Boot.cpp index e6ce8accfd..9316e719ee 100644 --- a/Source/Core/Core/Boot/Boot.cpp +++ b/Source/Core/Core/Boot/Boot.cpp @@ -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; diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 56c8dba3de..bcd2ba9645 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -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 diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index f0cc8511b7..82eed03039 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -256,6 +256,7 @@ const Info MAIN_ARAM_EXPANSION_SIZE{{System::Main, "Core", "ARAMExpansionSi const Info MAIN_GPU_DETERMINISM_MODE{{System::Main, "Core", "GPUDeterminismMode"}, "auto"}; const Info MAIN_OVERRIDE_BOOT_IOS{{System::Main, "Core", "OverrideBootIOS"}, -1}; +const Info MAIN_WII_IOS_LLE{{System::Main, "Core", "WiiIOSLLE"}, false}; GPUDeterminismMode GetGPUDeterminismMode() { @@ -333,6 +334,7 @@ const Info MAIN_DUMP_PATH{{System::Main, "General", "DumpPath"}, "" const Info MAIN_LOAD_PATH{{System::Main, "General", "LoadPath"}, ""}; const Info MAIN_RESOURCEPACK_PATH{{System::Main, "General", "ResourcePackPath"}, ""}; const Info MAIN_FS_PATH{{System::Main, "General", "NANDRootPath"}, ""}; +const Info MAIN_WII_LLE_DUMP_PATH{{System::Main, "General", "WiiIOSLLEDumpPath"}, ""}; const Info MAIN_WII_SD_CARD_IMAGE_PATH{{System::Main, "General", "WiiSDCardPath"}, ""}; const Info MAIN_WII_SD_CARD_SYNC_FOLDER_PATH{ {System::Main, "General", "WiiSDCardSyncFolder"}, ""}; diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index 16cae73e15..639abe8796 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -166,6 +166,7 @@ extern const Info MAIN_ENABLE_SAVESTATES; extern const Info MAIN_FALLBACK_REGION; extern const Info MAIN_REAL_WII_REMOTE_REPEAT_REPORTS; extern const Info MAIN_OVERRIDE_BOOT_IOS; +extern const Info MAIN_WII_IOS_LLE; extern const Info MAIN_WII_NUS_SHOP_URL; extern const Info MAIN_WII_WIILINK_ENABLE; @@ -207,6 +208,7 @@ extern const Info MAIN_DUMP_PATH; extern const Info MAIN_LOAD_PATH; extern const Info MAIN_RESOURCEPACK_PATH; extern const Info MAIN_FS_PATH; +extern const Info MAIN_WII_LLE_DUMP_PATH; extern const Info MAIN_WII_SD_CARD_IMAGE_PATH; extern const Info MAIN_WII_SD_CARD_SYNC_FOLDER_PATH; extern const Info MAIN_WFS_PATH; diff --git a/Source/Core/Core/ConfigManager.cpp b/Source/Core/Core/ConfigManager.cpp index 32f74a030f..cdb47e98ab 100644 --- a/Source/Core/Core/ConfigManager.cpp +++ b/Source/Core/Core/ConfigManager.cpp @@ -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)) diff --git a/Source/Core/Core/Core.cpp b/Source/Core/Core/Core.cpp index 1b50796e34..784004ad90 100644 --- a/Source/Core/Core/Core.cpp +++ b/Source/Core/Core/Core.cpp @@ -650,10 +650,14 @@ static void EmuThread(Core::System& system, std::unique_ptr 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(); diff --git a/Source/Core/Core/HW/DVD/DVDInterface.cpp b/Source/Core/Core/HW/DVD/DVDInterface.cpp index ce8cb72e42..5e20db1b4e 100644 --- a/Source/Core/Core/HW/DVD/DVDInterface.cpp +++ b/Source/Core/Core/HW/DVD/DVDInterface.cpp @@ -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); diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp index 82650ff1a0..00d84f0e9b 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp +++ b/Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp @@ -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 diff --git a/Source/Core/Core/HW/HW.cpp b/Source/Core/Core/HW/HW.cpp index 58b4beac03..1f157c74d9 100644 --- a/Source/Core/Core/HW/HW.cpp +++ b/Source/Core/Core/HW/HW.cpp @@ -3,7 +3,11 @@ #include "Core/HW/HW.h" +#include +#include + #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,7 +60,26 @@ void Init(Core::System& system, const Sram* override_sram) if (system.IsWii()) { system.GetWiiIPC().Init(); - IOS::HLE::Init(system); // Depends on Memory + if (Config::Get(Config::MAIN_WII_IOS_LLE)) + { + auto starlet = std::make_unique(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,7 +88,13 @@ 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). - IOS::HLE::Shutdown(system); // Depends on Memory + if (system.GetStarlet()) + { + system.GetStarlet()->Shutdown(); + system.SetStarlet(nullptr); + } + if (system.GetIOS()) + IOS::HLE::Shutdown(system); // Depends on Memory system.GetWiiIPC().Shutdown(); system.GetSystemTimers().Shutdown(); @@ -112,8 +142,16 @@ void DoState(Core::System& system, PointerWrap& p) { system.GetWiiIPC().DoState(p); p.DoMarker("IOS"); - system.GetIOS()->DoState(p); - p.DoMarker("IOS::HLE"); + 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"); diff --git a/Source/Core/Core/HW/Memmap.cpp b/Source/Core/Core/HW/Memmap.cpp index 054596e3d8..a809af1c11 100644 --- a/Source/Core/Core/HW/Memmap.cpp +++ b/Source/Core/Core/HW/Memmap.cpp @@ -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); } diff --git a/Source/Core/Core/HW/SystemTimers.cpp b/Source/Core/Core/HW/SystemTimers.cpp index 1790a97f05..8167203db3 100644 --- a/Source/Core/Core/HW/SystemTimers.cpp +++ b/Source/Core/Core/HW/SystemTimers.cpp @@ -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(); diff --git a/Source/Core/Core/HW/WII_IPC.cpp b/Source/Core/Core/HW/WII_IPC.cpp index c99c8afbf0..d366b7a997 100644 --- a/Source/Core/Core/HW/WII_IPC.cpp +++ b/Source/Core/Core/HW/WII_IPC.cpp @@ -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_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(), MMIO::DirectWrite(&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(&m_ppc_msg), + MMIO::ComplexWrite([](Core::System& system, u32, u32 val) { + auto& wii_ipc = system.GetWiiIPC(); + wii_ipc.m_ppc_msg = val; + })); mmio->Register(base | IPC_PPCCTRL, MMIO::ComplexRead([](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) - system.GetIOS()->EnqueueIPCRequest(wii_ipc.m_ppc_msg); - system.GetIOS()->UpdateIPC(); + { + 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(&m_arm_msg), MMIO::InvalidWrite()); + mmio->Register(base | IPC_ARMMSG, MMIO::DirectRead(&m_arm_msg), + MMIO::DirectWrite(&m_arm_msg)); - mmio->Register(base | PPC_IRQFLAG, MMIO::InvalidRead(), + mmio->Register(base | PPC_IRQFLAG, MMIO::DirectRead(&m_ppc_irq_flags), MMIO::ComplexWrite([](Core::System& system, u32, u32 val) { auto& wii_ipc = system.GetWiiIPC(); wii_ipc.m_ppc_irq_flags &= ~val; - system.GetIOS()->UpdateIPC(); + 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(), + mmio->Register(base | PPC_IRQMASK, MMIO::DirectRead(&m_ppc_irq_masks), MMIO::ComplexWrite([](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(); - system.GetIOS()->UpdateIPC(); + 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(0xFFFFFFFF), MMIO::InvalidWrite()); + // 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(&s_ahbprot), MMIO::InvalidWrite()); mmio->Register(base | GPIOB_OUT, MMIO::DirectRead(&m_gpio_out.m_hex), MMIO::ComplexWrite([](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()); - // 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(&m_gpio_out.m_hex), MMIO::ComplexWrite([](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([](Core::System& system, u32 address) { + IOS::LLE::Starlet* const starlet = system.GetStarlet(); + return starlet ? starlet->ReadHollywoodRegister(address) : 0; + }), + MMIO::ComplexWrite([](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([](Core::System& system, u32 address) { + IOS::LLE::Starlet* const starlet = system.GetStarlet(); + return starlet ? starlet->ReadHollywoodRegister(address) : 0; + }), + MMIO::InvalidWrite()); + // Register some stubbed/unknown MMIOs required to make Wii games work. mmio->Register(base | PPCSPEED, MMIO::InvalidRead(), MMIO::Nop()); mmio->Register(base | VISOLID, MMIO::InvalidRead(), MMIO::Nop()); @@ -275,14 +328,166 @@ void WiiIPC::UpdateInterrupts() if ((m_ctrl.X1 & m_ctrl.IX1) || (m_ctrl.X2 & m_ctrl.IX2)) { - m_ppc_irq_flags |= INT_CAUSE_IPC_STARLET; + 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); } diff --git a/Source/Core/Core/HW/WII_IPC.h b/Source/Core/Core/HW/WII_IPC.h index 64144f43c0..1d40b68a73 100644 --- a/Source/Core/Core/HW/WII_IPC.h +++ b/Source/Core/Core/HW/WII_IPC.h @@ -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 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 m_gpio_dir{}; Common::Flags m_gpio_out{}; u32 m_resets = 0; + bool m_broadway_in_reset = false; CoreTiming::EventType* m_event_type_update_interrupts = nullptr; diff --git a/Source/Core/Core/HW/Wiimote.cpp b/Source/Core/Core/HW/Wiimote.cpp index 63e2cbf4d2..2bd670d0ba 100644 --- a/Source/Core/Core/HW/Wiimote.cpp +++ b/Source/Core/Core/HW/Wiimote.cpp @@ -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; diff --git a/Source/Core/Core/IOS/Starlet/ARMCore.cpp b/Source/Core/Core/IOS/Starlet/ARMCore.cpp new file mode 100644 index 0000000000..064346260a --- /dev/null +++ b/Source/Core/Core/IOS/Starlet/ARMCore.cpp @@ -0,0 +1,1946 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/IOS/Starlet/ARMCore.h" + +#include +#include +#include +#include +#include + +#include "Common/ChunkFile.h" + +namespace IOS::LLE +{ +namespace +{ +constexpr u32 CP15_CONTROL_BIG_ENDIAN = 1U << 7; +constexpr u32 CP15_CONTROL_MMU = 1U << 0; +constexpr u32 STARLET_EXCEPTION_VECTOR_BASE = 0xffff0000; + +constexpr u32 ModeValue(ARMCore::Mode mode) +{ + return static_cast(mode); +} +} // namespace + +ARMCore::ARMCore(ARMBus& bus) : m_bus(bus) +{ + Reset(); +} + +void ARMCore::Reset(u32 reset_vector) +{ + m_registers.fill(0); + m_user_r8_r14.fill(0); + m_fiq_r8_r14.fill(0); + m_irq_r13_r14.fill(0); + m_svc_r13_r14.fill(0); + m_abt_r13_r14.fill(0); + m_und_r13_r14.fill(0); + m_spsr_fiq = 0; + m_spsr_irq = 0; + m_spsr_svc = 0; + m_spsr_abt = 0; + m_spsr_und = 0; + m_cp15 = {}; + InvalidateTLB(); + InvalidateInstructionCache(); + if (m_big_endian) + m_cp15.control |= CP15_CONTROL_BIG_ENDIAN; + + m_cpsr = ModeValue(Mode::Supervisor) | CPSR_I | CPSR_F; + m_registers[15] = reset_vector & ~3U; + m_irq_line = false; + m_fiq_line = false; + m_waiting_for_interrupt = false; + m_waiting_for_memory_poll = false; + m_yield_requested = false; + m_memory_poll_address = 0; + m_pc_written = false; + m_instruction_address = m_registers[15]; + m_last_undefined_instruction = 0; + m_executed_instructions = 0; +} + +u32 ARMCore::GetRegister(size_t index) const +{ + assert(index < m_registers.size()); + return m_registers[index]; +} + +void ARMCore::SetRegister(size_t index, u32 value) +{ + assert(index < m_registers.size()); + if (index == 15) + { + m_registers[index] = value & ((m_cpsr & CPSR_T) ? ~1U : ~3U); + return; + } + m_registers[index] = value; +} + +bool ARMCore::IsValidMode(u32 mode) +{ + switch (static_cast(mode)) + { + case Mode::User: + case Mode::FIQ: + case Mode::IRQ: + case Mode::Supervisor: + case Mode::Abort: + case Mode::Undefined: + case Mode::System: + return true; + default: + return false; + } +} + +ARMCore::Mode ARMCore::GetMode() const +{ + return static_cast(m_cpsr & CPSR_MODE_MASK); +} + +void ARMCore::SetCPSR(u32 value) +{ + const u32 new_mode_value = value & CPSR_MODE_MASK; + if (!IsValidMode(new_mode_value)) + value = (value & ~CPSR_MODE_MASK) | ModeValue(GetMode()); + + const Mode new_mode = static_cast(value & CPSR_MODE_MASK); + if (new_mode != GetMode()) + SwitchMode(new_mode); + m_cpsr = value; +} + +void ARMCore::SetBigEndian(bool big_endian) +{ + m_big_endian = big_endian; + if (big_endian) + m_cp15.control |= CP15_CONTROL_BIG_ENDIAN; + else + m_cp15.control &= ~CP15_CONTROL_BIG_ENDIAN; + InvalidateTLB(); + InvalidateInstructionCache(); +} + +s32 ARMCore::SignExtend(u32 value, unsigned bits) +{ + const u32 sign = 1U << (bits - 1); + return static_cast((value ^ sign) - sign); +} + +ARMCore::AddResult ARMCore::AddWithCarry(u32 lhs, u32 rhs, bool carry) +{ + const u64 unsigned_sum = static_cast(lhs) + rhs + static_cast(carry); + const s64 signed_sum = static_cast(static_cast(lhs)) + + static_cast(static_cast(rhs)) + static_cast(carry); + return { + .value = static_cast(unsigned_sum), + .carry = (unsigned_sum >> 32) != 0, + .overflow = signed_sum > std::numeric_limits::max() || + signed_sum < std::numeric_limits::min(), + }; +} + +u32 ARMCore::ReadPhysical32(u32 address) const +{ + const u32 value = m_bus.Read32(address); + if (m_big_endian) + return value; + return (value >> 24) | ((value >> 8) & 0x0000ff00) | ((value << 8) & 0x00ff0000) | (value << 24); +} + +u32 ARMCore::TranslateVirtualAddress(u32 address) const +{ + if ((m_cp15.control & CP15_CONTROL_MMU) == 0) + return address; + + // ARM926 FCSE remaps the low 32 MiB through the process identifier before + // translation. + const u32 modified_address = + address < 0x02000000 ? address | (m_cp15.process_id & 0xfe000000) : address; + const u32 virtual_page = modified_address >> 10; + TLBEntry& entry = m_tlb[virtual_page & (TLB_ENTRY_COUNT - 1)]; + if (entry.generation == m_tlb_generation && entry.virtual_page == virtual_page) + return entry.physical_page | (modified_address & 0x3ff); + + const auto cache_translation = [&](u32 physical_address) { + entry.virtual_page = virtual_page; + entry.physical_page = physical_address & ~0x3ffU; + entry.generation = m_tlb_generation; + return physical_address; + }; + + const u32 first_level_address = + (m_cp15.translation_table_base & 0xffffc000) | ((modified_address >> 18) & 0x3ffc); + const u32 first_level = ReadPhysical32(first_level_address); + switch (first_level & 3) + { + case 1: // Coarse second-level table. + { + const u32 second_level_address = + (first_level & 0xfffffc00) | ((modified_address >> 10) & 0x3fc); + const u32 second_level = ReadPhysical32(second_level_address); + switch (second_level & 3) + { + case 1: // 64 KiB large page. + return cache_translation((second_level & 0xffff0000) | (modified_address & 0xffff)); + case 2: + case 3: // 4 KiB small page; extended small pages share this mapping shape. + return cache_translation((second_level & 0xfffff000) | (modified_address & 0xfff)); + default: + return modified_address; + } + } + case 2: // 1 MiB section. + return cache_translation((first_level & 0xfff00000) | (modified_address & 0x000fffff)); + case 3: // Fine second-level table. + { + const u32 second_level_address = (first_level & 0xfffff000) | ((modified_address >> 8) & 0xffc); + const u32 second_level = ReadPhysical32(second_level_address); + switch (second_level & 3) + { + case 1: + return cache_translation((second_level & 0xffff0000) | (modified_address & 0xffff)); + case 2: + return cache_translation((second_level & 0xfffff000) | (modified_address & 0xfff)); + case 3: // 1 KiB tiny page. + return cache_translation((second_level & 0xfffffc00) | (modified_address & 0x3ff)); + default: + return modified_address; + } + } + default: + // Fault entry. Abort delivery and access-permission checks are + // intentionally introduced after basic page-table translation; identity + // fallback keeps diagnostics observable meanwhile. + return modified_address; + } +} + +void ARMCore::InvalidateTLB() +{ + if (++m_tlb_generation == 0) + { + for (TLBEntry& entry : m_tlb) + entry.generation = 0; + m_tlb_generation = 1; + } +} + +u32 ARMCore::FetchARMInstruction(u32 address) +{ + const u32 physical_address = TranslateVirtualAddress(address); + const u32 physical_word = physical_address & ~3U; + InstructionCacheEntry& entry = + m_instruction_cache[(physical_word >> 2) & (INSTRUCTION_CACHE_ENTRY_COUNT - 1)]; + if (entry.generation != m_instruction_cache_generation || entry.physical_word != physical_word) + { + entry.physical_word = physical_word; + entry.raw_value = m_bus.Read32(physical_word); + entry.generation = m_instruction_cache_generation; + } + + const u32 value = entry.raw_value; + if (m_big_endian) + return value; + return (value >> 24) | ((value >> 8) & 0x0000ff00) | ((value << 8) & 0x00ff0000) | (value << 24); +} + +u16 ARMCore::FetchThumbInstruction(u32 address) +{ + const u32 physical_address = TranslateVirtualAddress(address); + const u32 physical_word = physical_address & ~3U; + InstructionCacheEntry& entry = + m_instruction_cache[(physical_word >> 2) & (INSTRUCTION_CACHE_ENTRY_COUNT - 1)]; + if (entry.generation != m_instruction_cache_generation || entry.physical_word != physical_word) + { + entry.physical_word = physical_word; + entry.raw_value = m_bus.Read32(physical_word); + entry.generation = m_instruction_cache_generation; + } + + u16 value = static_cast((physical_address & 2) ? entry.raw_value : entry.raw_value >> 16); + if (!m_big_endian) + value = static_cast((value >> 8) | (value << 8)); + return value; +} + +void ARMCore::InvalidateInstructionCache() +{ + if (++m_instruction_cache_generation == 0) + { + for (InstructionCacheEntry& entry : m_instruction_cache) + entry.generation = 0; + m_instruction_cache_generation = 1; + } +} + +u8 ARMCore::ReadByte(u32 address) const +{ + return m_bus.Read8(TranslateVirtualAddress(address)); +} + +void ARMCore::WriteByte(u32 address, u8 value) +{ + m_bus.Write8(TranslateVirtualAddress(address), value); +} + +u16 ARMCore::Read16(u32 address) const +{ + const u16 value = + (address & 1) == 0 ? + m_bus.Read16(TranslateVirtualAddress(address)) : + static_cast((static_cast(m_bus.Read8(TranslateVirtualAddress(address))) << 8) | + m_bus.Read8(TranslateVirtualAddress(address + 1))); + return m_big_endian ? value : static_cast((value >> 8) | (value << 8)); +} + +u32 ARMCore::Read32(u32 address) const +{ + const u32 value = + (address & 3) == 0 ? + m_bus.Read32(TranslateVirtualAddress(address)) : + (static_cast(m_bus.Read8(TranslateVirtualAddress(address))) << 24) | + (static_cast(m_bus.Read8(TranslateVirtualAddress(address + 1))) << 16) | + (static_cast(m_bus.Read8(TranslateVirtualAddress(address + 2))) << 8) | + m_bus.Read8(TranslateVirtualAddress(address + 3)); + if (m_big_endian) + return value; + return (value >> 24) | ((value >> 8) & 0x0000ff00) | ((value << 8) & 0x00ff0000) | (value << 24); +} + +void ARMCore::Write16(u32 address, u16 value) +{ + if (!m_big_endian) + value = static_cast((value >> 8) | (value << 8)); + if ((address & 1) == 0) + { + m_bus.Write16(TranslateVirtualAddress(address), value); + return; + } + m_bus.Write8(TranslateVirtualAddress(address), static_cast(value >> 8)); + m_bus.Write8(TranslateVirtualAddress(address + 1), static_cast(value)); +} + +void ARMCore::Write32(u32 address, u32 value) +{ + if (!m_big_endian) + { + value = + (value >> 24) | ((value >> 8) & 0x0000ff00) | ((value << 8) & 0x00ff0000) | (value << 24); + } + if ((address & 3) == 0) + { + m_bus.Write32(TranslateVirtualAddress(address), value); + return; + } + m_bus.Write8(TranslateVirtualAddress(address), static_cast(value >> 24)); + m_bus.Write8(TranslateVirtualAddress(address + 1), static_cast(value >> 16)); + m_bus.Write8(TranslateVirtualAddress(address + 2), static_cast(value >> 8)); + m_bus.Write8(TranslateVirtualAddress(address + 3), static_cast(value)); +} + +u32 ARMCore::ReadRegisterOperand(u32 index) const +{ + assert(index < m_registers.size()); + if (index != 15) + return m_registers[index]; + return m_instruction_address + ((m_cpsr & CPSR_T) ? 4 : 8); +} + +u32 ARMCore::ReadRegisterForStore(u32 index) const +{ + assert(index < m_registers.size()); + if (index != 15) + return m_registers[index]; + // ARM9 exposes an implementation-defined PC value for stores. ARM926 uses + // instruction + 12. + return m_instruction_address + ((m_cpsr & CPSR_T) ? 4 : 12); +} + +void ARMCore::WritePC(u32 value, bool exchange) +{ + if (exchange) + { + if (value & 1) + m_cpsr |= CPSR_T; + else + m_cpsr &= ~CPSR_T; + } + + m_registers[15] = value & ((m_cpsr & CPSR_T) ? ~1U : ~3U); + m_pc_written = true; +} + +void ARMCore::SetNZ(u32 value) +{ + m_cpsr &= ~(CPSR_N | CPSR_Z); + if (value & 0x80000000) + m_cpsr |= CPSR_N; + if (value == 0) + m_cpsr |= CPSR_Z; +} + +bool ARMCore::ConditionPassed(u32 condition) const +{ + const bool n = (m_cpsr & CPSR_N) != 0; + const bool z = (m_cpsr & CPSR_Z) != 0; + const bool c = (m_cpsr & CPSR_C) != 0; + const bool v = (m_cpsr & CPSR_V) != 0; + switch (condition) + { + case 0x0: + return z; + case 0x1: + return !z; + case 0x2: + return c; + case 0x3: + return !c; + case 0x4: + return n; + case 0x5: + return !n; + case 0x6: + return v; + case 0x7: + return !v; + case 0x8: + return c && !z; + case 0x9: + return !c || z; + case 0xa: + return n == v; + case 0xb: + return n != v; + case 0xc: + return !z && n == v; + case 0xd: + return z || n != v; + case 0xe: + return true; + default: + return false; + } +} + +ARMCore::ShiftResult ARMCore::ShiftImmediate(u32 value, u32 type, u32 amount, bool old_carry) const +{ + switch (type) + { + case 0: // LSL + if (amount == 0) + return {value, old_carry}; + return {value << amount, ((value >> (32 - amount)) & 1) != 0}; + case 1: // LSR, immediate zero means 32 + if (amount == 0) + return {0, (value >> 31) != 0}; + return {value >> amount, ((value >> (amount - 1)) & 1) != 0}; + case 2: // ASR, immediate zero means 32 + if (amount == 0) + return {(value & 0x80000000) ? 0xffffffffU : 0, (value >> 31) != 0}; + return {static_cast(static_cast(value) >> amount), + ((value >> (amount - 1)) & 1) != 0}; + case 3: // ROR, immediate zero is RRX + if (amount == 0) + return {(static_cast(old_carry) << 31) | (value >> 1), (value & 1) != 0}; + return {std::rotr(value, static_cast(amount)), ((value >> (amount - 1)) & 1) != 0}; + default: + return {value, old_carry}; + } +} + +ARMCore::ShiftResult ARMCore::ShiftRegister(u32 value, u32 type, u32 amount, bool old_carry) const +{ + amount &= 0xff; + if (amount == 0) + return {value, old_carry}; + + switch (type) + { + case 0: // LSL + if (amount < 32) + return {value << amount, ((value >> (32 - amount)) & 1) != 0}; + return {0, amount == 32 && (value & 1) != 0}; + case 1: // LSR + if (amount < 32) + return {value >> amount, ((value >> (amount - 1)) & 1) != 0}; + return {0, amount == 32 && (value >> 31) != 0}; + case 2: // ASR + if (amount < 32) + return {static_cast(static_cast(value) >> amount), + ((value >> (amount - 1)) & 1) != 0}; + return {(value & 0x80000000) ? 0xffffffffU : 0, (value >> 31) != 0}; + case 3: // ROR + { + const u32 rotate = amount & 31; + if (rotate == 0) + return {value, (value >> 31) != 0}; + return {std::rotr(value, static_cast(rotate)), ((value >> (rotate - 1)) & 1) != 0}; + } + default: + return {value, old_carry}; + } +} + +ARMCore::ShiftResult ARMCore::DecodeOperand2(u32 instruction) const +{ + const bool old_carry = (m_cpsr & CPSR_C) != 0; + if (instruction & (1U << 25)) + { + const u32 rotate = ((instruction >> 8) & 0xf) * 2; + const u32 value = std::rotr(instruction & 0xff, static_cast(rotate)); + return {value, rotate == 0 ? old_carry : (value >> 31) != 0}; + } + + const u32 rm_value = ReadRegisterOperand(instruction & 0xf); + const u32 type = (instruction >> 5) & 3; + if (instruction & (1U << 4)) + { + const u32 amount = ReadRegisterOperand((instruction >> 8) & 0xf) & 0xff; + return ShiftRegister(rm_value, type, amount, old_carry); + } + return ShiftImmediate(rm_value, type, (instruction >> 7) & 0x1f, old_carry); +} + +int ARMCore::Step() +{ + return StepInternal(true); +} + +bool ARMCore::HasUnmaskedInterrupt() const +{ + return (m_irq_line && (m_cpsr & CPSR_I) == 0) || (m_fiq_line && (m_cpsr & CPSR_F) == 0); +} + +bool ARMCore::TryEnterThumbMemoryPoll(u16 branch_instruction) +{ + // IOS uses a tiny Thumb idle loop which repeatedly loads a shared word, compares it with zero, + // and branches back while no scheduler work is pending. Replaying that side-effect-free loop at + // the full 243 MHz Starlet clock dominates the host CPU. Recognize the general instruction shape + // and fast-forward only physical RAM/SRAM polls; MMIO polling remains fully interpreted. + if ((branch_instruction & 0xff00) != 0xd000 || !m_pc_written || HasUnmaskedInterrupt()) + return false; + + const u32 loop_address = m_registers[15]; + if (loop_address + 4 != m_instruction_address) + return false; + + const u16 load = FetchThumbInstruction(loop_address); + const u16 compare = FetchThumbInstruction(loop_address + 2); + if ((load & 0xf800) != 0x6800 || (compare & 0xf800) != 0x2800 || (compare & 0xff) != 0) + { + return false; + } + + const u32 destination_register = load & 7; + if (static_cast((compare >> 8) & 7) != destination_register || + m_registers[destination_register] != 0) + return false; + + const u32 base_register = (load >> 3) & 7; + const u32 address = m_registers[base_register] + (((load >> 6) & 0x1f) << 2); + if ((address & 3) != 0) + return false; + const u32 physical_address = TranslateVirtualAddress(address); + if (!m_bus.IsIdlePollAddress(physical_address, sizeof(u32))) + return false; + + m_memory_poll_address = address; + m_waiting_for_memory_poll = true; + return true; +} + +int ARMCore::StepInternal(bool advance_bus) +{ + // ARM926's wait-for-interrupt operation stops instruction issue while clocks + // used by timers and peripherals continue running. A pending IRQ/FIQ wakes + // the core even when the corresponding CPSR mask prevents immediate exception + // entry. + if (m_waiting_for_memory_poll) + { + if (!HasUnmaskedInterrupt() && Read32(m_memory_poll_address) == 0) + { + if (advance_bus) + m_bus.AdvanceCycles(1); + return 1; + } + m_waiting_for_memory_poll = false; + } + if (m_waiting_for_interrupt && !m_irq_line && !m_fiq_line) + { + if (advance_bus) + m_bus.AdvanceCycles(1); + return 1; + } + if (m_irq_line || m_fiq_line) + m_waiting_for_interrupt = false; + + // Starlet boots from and vectors through the high SRAM window independently + // of the generic ARM926 control-register V bit. + constexpr u32 vector_base = STARLET_EXCEPTION_VECTOR_BASE; + if (m_fiq_line && !(m_cpsr & CPSR_F)) + { + EnterException(Mode::FIQ, vector_base + 0x1c, m_registers[15] + 4, true); + if (advance_bus) + m_bus.AdvanceCycles(3); + return 3; + } + if (m_irq_line && !(m_cpsr & CPSR_I)) + { + EnterException(Mode::IRQ, vector_base + 0x18, m_registers[15] + 4); + if (advance_bus) + m_bus.AdvanceCycles(3); + return 3; + } + + m_instruction_address = m_registers[15]; + m_pc_written = false; + if (m_cpsr & CPSR_T) + { + const u16 instruction = FetchThumbInstruction(m_instruction_address); + ExecuteThumb(instruction); + TryEnterThumbMemoryPoll(instruction); + } + else + { + const u32 instruction = FetchARMInstruction(m_instruction_address); + + // BLX immediate uses the otherwise-reserved condition value 0xf. + if ((instruction & 0xfe000000) == 0xfa000000) + { + const u32 h = (instruction >> 24) & 1; + const s32 offset = SignExtend((instruction & 0x00ffffff) << 2, 26) + static_cast(h << 1); + m_registers[14] = m_instruction_address + 4; + m_cpsr |= CPSR_T; + WritePC(m_instruction_address + 8 + static_cast(offset)); + } + else if (ConditionPassed(instruction >> 28)) + { + ExecuteARM(instruction); + } + } + + if (!m_pc_written) + m_registers[15] = m_instruction_address + ((m_cpsr & CPSR_T) ? 2 : 4); + ++m_executed_instructions; + if (advance_bus) + m_bus.AdvanceCycles(1); + return 1; +} + +u64 ARMCore::Run(u64 instruction_count) +{ + u64 cycles = 0; + while (instruction_count-- != 0) + cycles += static_cast(Step()); + return cycles; +} + +u64 ARMCore::RunCycles(u64 cycle_budget) +{ + u64 cycles = 0; + while (cycles < cycle_budget && !m_yield_requested) + { + if (m_waiting_for_memory_poll) + { + if (HasUnmaskedInterrupt() || Read32(m_memory_poll_address) != 0) + { + m_waiting_for_memory_poll = false; + } + else + { + cycles = cycle_budget; + break; + } + } + if (m_waiting_for_interrupt && !m_irq_line && !m_fiq_line) + { + cycles = cycle_budget; + break; + } + cycles += static_cast(StepInternal(false)); + } + // Starlet interrupt lines are sampled by the scheduler at slice boundaries. + // Updating timers and peripherals once here is therefore architecturally + // equivalent for interrupt delivery and avoids running the complete device + // clock path after every interpreted instruction. + m_bus.AdvanceCycles(cycles); + m_yield_requested = false; + return cycles; +} + +void ARMCore::DoState(PointerWrap& p) +{ + p.DoArray(m_registers); + p.Do(m_cpsr); + p.DoArray(m_user_r8_r14); + p.DoArray(m_fiq_r8_r14); + p.DoArray(m_irq_r13_r14); + p.DoArray(m_svc_r13_r14); + p.DoArray(m_abt_r13_r14); + p.DoArray(m_und_r13_r14); + p.Do(m_spsr_fiq); + p.Do(m_spsr_irq); + p.Do(m_spsr_svc); + p.Do(m_spsr_abt); + p.Do(m_spsr_und); + p.Do(m_cp15); + p.Do(m_big_endian); + p.Do(m_irq_line); + p.Do(m_fiq_line); + p.Do(m_waiting_for_interrupt); + p.Do(m_waiting_for_memory_poll); + p.Do(m_yield_requested); + p.Do(m_memory_poll_address); + p.Do(m_pc_written); + p.Do(m_instruction_address); + p.Do(m_last_undefined_instruction); + p.Do(m_executed_instructions); + // The cache is derived entirely from architectural CP15 state and guest page + // tables. + InvalidateTLB(); + InvalidateInstructionCache(); +} + +void ARMCore::ExecuteARM(u32 instruction) +{ + // Branch/exchange and count-leading-zero are ARMv5 additions encoded in the + // data-processing space, so they must be recognized before the generic + // decoder. + if ((instruction & 0x0ffffff0) == 0x012fff10) // BX + { + WritePC(ReadRegisterOperand(instruction & 0xf), true); + return; + } + if ((instruction & 0x0ffffff0) == 0x012fff30) // BLX register + { + const u32 target = ReadRegisterOperand(instruction & 0xf); + m_registers[14] = m_instruction_address + 4; + WritePC(target, true); + return; + } + if ((instruction & 0x0fff0ff0) == 0x016f0f10) // CLZ + { + const u32 rd = (instruction >> 12) & 0xf; + const u32 value = ReadRegisterOperand(instruction & 0xf); + m_registers[rd] = value == 0 ? 32 : std::countl_zero(value); + return; + } + + if ((instruction & 0x0f8000f0) == 0x00800090) + { + ExecuteMultiplyLong(instruction); + return; + } + if ((instruction & 0x0fc000f0) == 0x00000090) + { + ExecuteMultiply(instruction); + return; + } + + // SWP/SWPB. + if ((instruction & 0x0fb00ff0) == 0x01000090) + { + const bool byte = (instruction & (1U << 22)) != 0; + const u32 rn = (instruction >> 16) & 0xf; + const u32 rd = (instruction >> 12) & 0xf; + const u32 rm = instruction & 0xf; + const u32 address = ReadRegisterOperand(rn); + if (byte) + { + const u8 old = ReadByte(address); + WriteByte(address, static_cast(ReadRegisterOperand(rm))); + m_registers[rd] = old; + } + else + { + const u32 old = Read32(address & ~3U); + Write32(address & ~3U, ReadRegisterOperand(rm)); + m_registers[rd] = old; + } + return; + } + + const bool is_mrs = (instruction & 0x0fbf0fff) == 0x010f0000; + const bool is_msr_register = (instruction & 0x0fb0fff0) == 0x0120f000; + const bool is_msr_immediate = (instruction & 0x0fb0f000) == 0x0320f000; + if (is_mrs || is_msr_register || is_msr_immediate) + { + ExecutePSRTransfer(instruction); + return; + } + + if ((instruction & 0x0e000090) == 0x00000090) + { + ExecuteHalfwordDataTransfer(instruction); + return; + } + + switch ((instruction >> 25) & 7) + { + case 0: + case 1: + ExecuteDataProcessing(instruction); + return; + case 2: + case 3: + ExecuteSingleDataTransfer(instruction); + return; + case 4: + ExecuteBlockDataTransfer(instruction); + return; + case 5: + ExecuteBranch(instruction); + return; + case 7: + if ((instruction & 0x0f000000) == 0x0f000000) + { + EnterException(Mode::Supervisor, STARLET_EXCEPTION_VECTOR_BASE + 0x08, + m_instruction_address + 4); + return; + } + if (instruction & (1U << 4)) + { + ExecuteCoprocessor(instruction); + return; + } + break; + default: + break; + } + + UndefinedInstruction(instruction); +} + +void ARMCore::ExecuteThumb(u16 instruction) +{ + const auto set_logical_flags = [this](u32 value, bool carry, bool update_carry) { + const bool old_overflow = (m_cpsr & CPSR_V) != 0; + SetNZ(value); + if (update_carry) + { + m_cpsr &= ~CPSR_C; + if (carry) + m_cpsr |= CPSR_C; + } + if (old_overflow) + m_cpsr |= CPSR_V; + }; + const auto set_arithmetic_flags = [this](const AddResult& result) { + SetNZ(result.value); + m_cpsr &= ~(CPSR_C | CPSR_V); + if (result.carry) + m_cpsr |= CPSR_C; + if (result.overflow) + m_cpsr |= CPSR_V; + }; + + // Move shifted register and add/subtract. + if ((instruction & 0xe000) == 0x0000) + { + if ((instruction & 0x1800) == 0x1800) + { + const bool immediate = (instruction & 0x0400) != 0; + const bool subtract = (instruction & 0x0200) != 0; + const u32 operand = + immediate ? ((instruction >> 6) & 7) : m_registers[(instruction >> 6) & 7]; + const u32 rs = (instruction >> 3) & 7; + const u32 rd = instruction & 7; + const AddResult result = subtract ? AddWithCarry(m_registers[rs], ~operand, true) : + AddWithCarry(m_registers[rs], operand, false); + m_registers[rd] = result.value; + set_arithmetic_flags(result); + return; + } + + const u32 type = (instruction >> 11) & 3; + const u32 amount = (instruction >> 6) & 0x1f; + const u32 rs = (instruction >> 3) & 7; + const u32 rd = instruction & 7; + const ShiftResult result = + ShiftImmediate(m_registers[rs], type, amount, (m_cpsr & CPSR_C) != 0); + m_registers[rd] = result.value; + set_logical_flags(result.value, result.carry, true); + return; + } + + // Move/compare/add/subtract immediate. + if ((instruction & 0xe000) == 0x2000) + { + const u32 opcode = (instruction >> 11) & 3; + const u32 rd = (instruction >> 8) & 7; + const u32 immediate = instruction & 0xff; + if (opcode == 0) // MOV + { + m_registers[rd] = immediate; + set_logical_flags(immediate, false, false); + } + else + { + const bool subtract = opcode == 1 || opcode == 3; + const u32 lhs = m_registers[rd]; + const AddResult result = + subtract ? AddWithCarry(lhs, ~immediate, true) : AddWithCarry(lhs, immediate, false); + if (opcode != 1) // CMP does not write. + m_registers[rd] = result.value; + set_arithmetic_flags(result); + } + return; + } + + // ALU operations. + if ((instruction & 0xfc00) == 0x4000) + { + const u32 opcode = (instruction >> 6) & 0xf; + const u32 rs = (instruction >> 3) & 7; + const u32 rd = instruction & 7; + const u32 lhs = m_registers[rd]; + const u32 rhs = m_registers[rs]; + u32 result = 0; + bool write_result = true; + bool arithmetic = false; + bool update_carry = false; + bool carry = false; + AddResult add{}; + switch (opcode) + { + case 0x0: // AND + result = lhs & rhs; + break; + case 0x1: // EOR + result = lhs ^ rhs; + break; + case 0x2: // LSL + case 0x3: // LSR + case 0x4: // ASR + case 0x7: // ROR + { + const u32 type = opcode == 0x7 ? 3 : opcode - 2; + const ShiftResult shifted = ShiftRegister(lhs, type, rhs & 0xff, (m_cpsr & CPSR_C) != 0); + result = shifted.value; + carry = shifted.carry; + update_carry = true; + break; + } + case 0x5: // ADC + add = AddWithCarry(lhs, rhs, (m_cpsr & CPSR_C) != 0); + result = add.value; + arithmetic = true; + break; + case 0x6: // SBC + add = AddWithCarry(lhs, ~rhs, (m_cpsr & CPSR_C) != 0); + result = add.value; + arithmetic = true; + break; + case 0x8: // TST + result = lhs & rhs; + write_result = false; + break; + case 0x9: // NEG + add = AddWithCarry(0, ~rhs, true); + result = add.value; + arithmetic = true; + break; + case 0xa: // CMP + add = AddWithCarry(lhs, ~rhs, true); + result = add.value; + arithmetic = true; + write_result = false; + break; + case 0xb: // CMN + add = AddWithCarry(lhs, rhs, false); + result = add.value; + arithmetic = true; + write_result = false; + break; + case 0xc: // ORR + result = lhs | rhs; + break; + case 0xd: // MUL + result = lhs * rhs; + break; + case 0xe: // BIC + result = lhs & ~rhs; + break; + case 0xf: // MVN + result = ~rhs; + break; + } + + if (write_result) + m_registers[rd] = result; + if (arithmetic) + set_arithmetic_flags(add); + else + set_logical_flags(result, carry, update_carry); + return; + } + + // High-register operations and BX/BLX. + if ((instruction & 0xfc00) == 0x4400) + { + const u32 opcode = (instruction >> 8) & 3; + const u32 rd = (instruction & 7) | ((instruction >> 4) & 8); + const u32 rs = ((instruction >> 3) & 7) | ((instruction >> 3) & 8); + const u32 rhs = ReadRegisterOperand(rs); + if (opcode == 0) // ADD + { + const u32 result = ReadRegisterOperand(rd) + rhs; + if (rd == 15) + WritePC(result); + else + m_registers[rd] = result; + } + else if (opcode == 1) // CMP + { + set_arithmetic_flags(AddWithCarry(ReadRegisterOperand(rd), ~rhs, true)); + } + else if (opcode == 2) // MOV + { + if (rd == 15) + WritePC(rhs); + else + m_registers[rd] = rhs; + } + else // BX/BLX + { + if (instruction & 0x0080) + m_registers[14] = (m_instruction_address + 2) | 1; + WritePC(rhs, true); + } + return; + } + + // PC-relative load. + if ((instruction & 0xf800) == 0x4800) + { + const u32 rd = (instruction >> 8) & 7; + const u32 address = ((m_instruction_address + 4) & ~3U) + ((instruction & 0xff) << 2); + m_registers[rd] = Read32(address); + return; + } + + // Register-offset loads and stores. + if ((instruction & 0xf000) == 0x5000) + { + const u32 operation = (instruction >> 9) & 7; + const u32 ro = (instruction >> 6) & 7; + const u32 rb = (instruction >> 3) & 7; + const u32 rd = instruction & 7; + const u32 address = m_registers[rb] + m_registers[ro]; + switch (operation) + { + case 0: // STR + Write32(address & ~3U, m_registers[rd]); + break; + case 1: // STRH + Write16(address, static_cast(m_registers[rd])); + break; + case 2: // STRB + WriteByte(address, static_cast(m_registers[rd])); + break; + case 3: // LDRSB + m_registers[rd] = static_cast(static_cast(static_cast(ReadByte(address)))); + break; + case 4: // LDR + m_registers[rd] = std::rotr(Read32(address & ~3U), static_cast((address & 3) * 8)); + break; + case 5: // LDRH + m_registers[rd] = Read16(address); + break; + case 6: // LDRB + m_registers[rd] = ReadByte(address); + break; + case 7: // LDRSH + m_registers[rd] = static_cast(static_cast(static_cast(Read16(address)))); + break; + } + return; + } + + // Immediate-offset word/byte loads and stores. + if ((instruction & 0xe000) == 0x6000) + { + const bool byte = (instruction & 0x1000) != 0; + const bool load = (instruction & 0x0800) != 0; + const u32 offset = ((instruction >> 6) & 0x1f) << (byte ? 0 : 2); + const u32 rb = (instruction >> 3) & 7; + const u32 rd = instruction & 7; + const u32 address = m_registers[rb] + offset; + if (load) + { + m_registers[rd] = byte ? ReadByte(address) : Read32(address); + } + else if (byte) + { + WriteByte(address, static_cast(m_registers[rd])); + } + else + { + Write32(address, m_registers[rd]); + } + return; + } + + // Immediate-offset halfword loads and stores. + if ((instruction & 0xf000) == 0x8000) + { + const bool load = (instruction & 0x0800) != 0; + const u32 address = m_registers[(instruction >> 3) & 7] + (((instruction >> 6) & 0x1f) << 1); + const u32 rd = instruction & 7; + if (load) + m_registers[rd] = Read16(address); + else + Write16(address, static_cast(m_registers[rd])); + return; + } + + // SP-relative loads and stores. + if ((instruction & 0xf000) == 0x9000) + { + const bool load = (instruction & 0x0800) != 0; + const u32 rd = (instruction >> 8) & 7; + const u32 address = m_registers[13] + ((instruction & 0xff) << 2); + if (load) + m_registers[rd] = Read32(address); + else + Write32(address, m_registers[rd]); + return; + } + + // Load address relative to PC or SP. + if ((instruction & 0xf000) == 0xa000) + { + const bool use_sp = (instruction & 0x0800) != 0; + const u32 rd = (instruction >> 8) & 7; + const u32 base = use_sp ? m_registers[13] : ((m_instruction_address + 4) & ~3U); + m_registers[rd] = base + ((instruction & 0xff) << 2); + return; + } + + // Add/subtract immediate to SP. + if ((instruction & 0xff00) == 0xb000) + { + const u32 offset = (instruction & 0x7f) << 2; + if (instruction & 0x0080) + m_registers[13] -= offset; + else + m_registers[13] += offset; + return; + } + + // PUSH/POP. + if ((instruction & 0xf600) == 0xb400) + { + const bool pop = (instruction & 0x0800) != 0; + const bool extra = (instruction & 0x0100) != 0; + const u32 register_list = instruction & 0xff; + const u32 count = std::popcount(register_list) + static_cast(extra); + if (count == 0) + { + UndefinedInstruction(instruction); + return; + } + + if (!pop) + { + u32 address = m_registers[13] - count * 4; + m_registers[13] = address; + for (u32 reg = 0; reg < 8; ++reg) + { + if (register_list & (1U << reg)) + { + Write32(address, m_registers[reg]); + address += 4; + } + } + if (extra) + Write32(address, m_registers[14]); + } + else + { + u32 address = m_registers[13]; + for (u32 reg = 0; reg < 8; ++reg) + { + if (register_list & (1U << reg)) + { + m_registers[reg] = Read32(address); + address += 4; + } + } + if (extra) + { + const u32 pc = Read32(address); + address += 4; + WritePC(pc, true); + } + m_registers[13] = address; + } + return; + } + + if ((instruction & 0xff00) == 0xbe00) // BKPT + { + UndefinedInstruction(instruction); + return; + } + + // Multiple load/store. + if ((instruction & 0xf000) == 0xc000) + { + const bool load = (instruction & 0x0800) != 0; + const u32 rb = (instruction >> 8) & 7; + const u32 register_list = instruction & 0xff; + if (register_list == 0) + { + UndefinedInstruction(instruction); + return; + } + u32 address = m_registers[rb]; + for (u32 reg = 0; reg < 8; ++reg) + { + if (!(register_list & (1U << reg))) + continue; + if (load) + m_registers[reg] = Read32(address); + else + Write32(address, m_registers[reg]); + address += 4; + } + if (!(load && (register_list & (1U << rb)))) + m_registers[rb] = address; + return; + } + + // Conditional branch and SWI. + if ((instruction & 0xf000) == 0xd000) + { + const u32 condition = (instruction >> 8) & 0xf; + if (condition == 0xf) + { + EnterException(Mode::Supervisor, STARLET_EXCEPTION_VECTOR_BASE + 0x08, + m_instruction_address + 2); + } + else if (condition == 0xe) + { + UndefinedInstruction(instruction); + } + else if (ConditionPassed(condition)) + { + const s32 offset = SignExtend(instruction & 0xff, 8) * 2; + WritePC(m_instruction_address + 4 + static_cast(offset)); + } + return; + } + + // Unconditional branch. + if ((instruction & 0xf800) == 0xe000) + { + const s32 offset = SignExtend(instruction & 0x7ff, 11) * 2; + WritePC(m_instruction_address + 4 + static_cast(offset)); + return; + } + + // BL/BLX first and second half. + if ((instruction & 0xf800) == 0xf000) + { + const s32 high_offset = SignExtend(instruction & 0x7ff, 11) * 4096; + m_registers[14] = m_instruction_address + 4 + static_cast(high_offset); + return; + } + if ((instruction & 0xf800) == 0xf800 || (instruction & 0xf800) == 0xe800) + { + const bool exchange = (instruction & 0xf800) == 0xe800; + const u32 target = m_registers[14] + ((instruction & 0x7ff) << 1); + m_registers[14] = (m_instruction_address + 2) | 1; + if (exchange) + m_cpsr &= ~CPSR_T; + WritePC(target); + return; + } + + UndefinedInstruction(instruction); +} + +void ARMCore::ExecuteDataProcessing(u32 instruction) +{ + const u32 opcode = (instruction >> 21) & 0xf; + const bool set_flags = (instruction & (1U << 20)) != 0; + const u32 rn = (instruction >> 16) & 0xf; + const u32 rd = (instruction >> 12) & 0xf; + const u32 lhs = ReadRegisterOperand(rn); + const ShiftResult operand2 = DecodeOperand2(instruction); + + u32 result = 0; + bool write_result = true; + bool arithmetic = false; + AddResult add{}; + switch (opcode) + { + case 0x0: // AND + result = lhs & operand2.value; + break; + case 0x1: // EOR + result = lhs ^ operand2.value; + break; + case 0x2: // SUB + add = AddWithCarry(lhs, ~operand2.value, true); + result = add.value; + arithmetic = true; + break; + case 0x3: // RSB + add = AddWithCarry(operand2.value, ~lhs, true); + result = add.value; + arithmetic = true; + break; + case 0x4: // ADD + add = AddWithCarry(lhs, operand2.value, false); + result = add.value; + arithmetic = true; + break; + case 0x5: // ADC + add = AddWithCarry(lhs, operand2.value, (m_cpsr & CPSR_C) != 0); + result = add.value; + arithmetic = true; + break; + case 0x6: // SBC + add = AddWithCarry(lhs, ~operand2.value, (m_cpsr & CPSR_C) != 0); + result = add.value; + arithmetic = true; + break; + case 0x7: // RSC + add = AddWithCarry(operand2.value, ~lhs, (m_cpsr & CPSR_C) != 0); + result = add.value; + arithmetic = true; + break; + case 0x8: // TST + result = lhs & operand2.value; + write_result = false; + break; + case 0x9: // TEQ + result = lhs ^ operand2.value; + write_result = false; + break; + case 0xa: // CMP + add = AddWithCarry(lhs, ~operand2.value, true); + result = add.value; + arithmetic = true; + write_result = false; + break; + case 0xb: // CMN + add = AddWithCarry(lhs, operand2.value, false); + result = add.value; + arithmetic = true; + write_result = false; + break; + case 0xc: // ORR + result = lhs | operand2.value; + break; + case 0xd: // MOV + result = operand2.value; + break; + case 0xe: // BIC + result = lhs & ~operand2.value; + break; + case 0xf: // MVN + result = ~operand2.value; + break; + } + + if (set_flags || !write_result) + { + const bool old_overflow = (m_cpsr & CPSR_V) != 0; + SetNZ(result); + m_cpsr &= ~(CPSR_C | CPSR_V); + if (arithmetic) + { + if (add.carry) + m_cpsr |= CPSR_C; + if (add.overflow) + m_cpsr |= CPSR_V; + } + else + { + if (operand2.carry) + m_cpsr |= CPSR_C; + // Logical operations leave V unchanged. + if (old_overflow) + m_cpsr |= CPSR_V; + } + } + + if (!write_result) + return; + if (rd == 15) + { + if (set_flags) + RestoreCPSRFromSPSR(); + WritePC(result); + } + else + { + m_registers[rd] = result; + } +} + +void ARMCore::ExecuteSingleDataTransfer(u32 instruction) +{ + const bool register_offset = (instruction & (1U << 25)) != 0; + const bool preindex = (instruction & (1U << 24)) != 0; + const bool add_offset = (instruction & (1U << 23)) != 0; + const bool byte = (instruction & (1U << 22)) != 0; + const bool writeback = (instruction & (1U << 21)) != 0; + const bool load = (instruction & (1U << 20)) != 0; + const u32 rn = (instruction >> 16) & 0xf; + const u32 rd = (instruction >> 12) & 0xf; + + u32 offset; + if (register_offset) + { + if (instruction & (1U << 4)) + { + UndefinedInstruction(instruction); + return; + } + const u32 rm = ReadRegisterOperand(instruction & 0xf); + offset = ShiftImmediate(rm, (instruction >> 5) & 3, (instruction >> 7) & 0x1f, + (m_cpsr & CPSR_C) != 0) + .value; + } + else + { + offset = instruction & 0xfff; + } + + const u32 base = ReadRegisterOperand(rn); + const u32 indexed = add_offset ? base + offset : base - offset; + const u32 address = preindex ? indexed : base; + if (load) + { + u32 value; + if (byte) + { + value = ReadByte(address); + } + else + { + const u32 aligned_value = Read32(address & ~3U); + value = std::rotr(aligned_value, static_cast((address & 3) * 8)); + } + + if (rd == 15) + WritePC(value, true); + else + m_registers[rd] = value; + } + else + { + const u32 value = ReadRegisterForStore(rd); + if (byte) + WriteByte(address, static_cast(value)); + else + Write32(address & ~3U, value); + } + + if ((!preindex || writeback) && !(load && rn == rd)) + m_registers[rn] = indexed; +} + +void ARMCore::ExecuteHalfwordDataTransfer(u32 instruction) +{ + const bool preindex = (instruction & (1U << 24)) != 0; + const bool add_offset = (instruction & (1U << 23)) != 0; + const bool immediate = (instruction & (1U << 22)) != 0; + const bool writeback = (instruction & (1U << 21)) != 0; + const bool load = (instruction & (1U << 20)) != 0; + const u32 rn = (instruction >> 16) & 0xf; + const u32 rd = (instruction >> 12) & 0xf; + const u32 type = (instruction >> 5) & 3; + const u32 offset = immediate ? (((instruction >> 4) & 0xf0) | (instruction & 0xf)) : + ReadRegisterOperand(instruction & 0xf); + const u32 base = ReadRegisterOperand(rn); + const u32 indexed = add_offset ? base + offset : base - offset; + const u32 address = preindex ? indexed : base; + + if (load) + { + u32 value; + switch (type) + { + case 1: // LDRH + value = Read16(address); + break; + case 2: // LDRSB + value = static_cast(static_cast(static_cast(ReadByte(address)))); + break; + case 3: // LDRSH + value = static_cast(static_cast(static_cast(Read16(address)))); + break; + default: + UndefinedInstruction(instruction); + return; + } + if (rd == 15) + WritePC(value); + else + m_registers[rd] = value; + } + else + { + if (type != 1) + { + // ARMv5 also uses this space for LDRD/STRD. They are deliberately + // rejected until their alignment and abort behavior is implemented. + UndefinedInstruction(instruction); + return; + } + Write16(address, static_cast(ReadRegisterForStore(rd))); + } + + if ((!preindex || writeback) && !(load && rn == rd)) + m_registers[rn] = indexed; +} + +void ARMCore::ExecuteBlockDataTransfer(u32 instruction) +{ + const bool preindex = (instruction & (1U << 24)) != 0; + const bool increment = (instruction & (1U << 23)) != 0; + const bool psr_or_user = (instruction & (1U << 22)) != 0; + const bool writeback = (instruction & (1U << 21)) != 0; + const bool load = (instruction & (1U << 20)) != 0; + const u32 rn = (instruction >> 16) & 0xf; + const u32 register_list = instruction & 0xffff; + if (register_list == 0) + { + UndefinedInstruction(instruction); + return; + } + + const u32 count = std::popcount(register_list); + const u32 base = ReadRegisterOperand(rn); + const bool user_bank_transfer = psr_or_user && !(load && (register_list & (1U << 15)) != 0); + const auto read_user_register = [this](u32 reg) { + if (GetMode() == Mode::User || GetMode() == Mode::System || reg < 8 || + (reg < 13 && GetMode() != Mode::FIQ)) + return ReadRegisterForStore(reg); + if (reg < 15) + return m_user_r8_r14[reg - 8]; + return ReadRegisterForStore(reg); + }; + const auto write_user_register = [this](u32 reg, u32 value) { + if (GetMode() == Mode::User || GetMode() == Mode::System || reg < 8 || + (reg < 13 && GetMode() != Mode::FIQ)) + m_registers[reg] = value; + else if (reg < 15) + m_user_r8_r14[reg - 8] = value; + }; + u32 address; + if (increment) + address = preindex ? base + 4 : base; + else + address = preindex ? base - count * 4 : base - (count - 1) * 4; + + u32 loaded_pc = 0; + bool pc_loaded = false; + for (u32 reg = 0; reg < 16; ++reg) + { + if (!(register_list & (1U << reg))) + continue; + if (load) + { + const u32 value = Read32(address); + if (reg == 15) + { + loaded_pc = value; + pc_loaded = true; + } + else + { + if (user_bank_transfer) + write_user_register(reg, value); + else + m_registers[reg] = value; + } + } + else + { + Write32(address, user_bank_transfer ? read_user_register(reg) : ReadRegisterForStore(reg)); + } + address += 4; + } + + if (writeback && !(load && (register_list & (1U << rn)))) + m_registers[rn] = increment ? base + count * 4 : base - count * 4; + + if (pc_loaded) + { + if (psr_or_user) + RestoreCPSRFromSPSR(); + WritePC(loaded_pc, true); + } +} + +void ARMCore::ExecuteMultiply(u32 instruction) +{ + const bool accumulate = (instruction & (1U << 21)) != 0; + const bool set_flags = (instruction & (1U << 20)) != 0; + const u32 rd = (instruction >> 16) & 0xf; + const u32 rn = (instruction >> 12) & 0xf; + const u32 rs = (instruction >> 8) & 0xf; + const u32 rm = instruction & 0xf; + u32 result = ReadRegisterOperand(rm) * ReadRegisterOperand(rs); + if (accumulate) + result += ReadRegisterOperand(rn); + m_registers[rd] = result; + if (set_flags) + SetNZ(result); +} + +void ARMCore::ExecuteMultiplyLong(u32 instruction) +{ + const bool signed_multiply = (instruction & (1U << 22)) != 0; + const bool accumulate = (instruction & (1U << 21)) != 0; + const bool set_flags = (instruction & (1U << 20)) != 0; + const u32 rd_hi = (instruction >> 16) & 0xf; + const u32 rd_lo = (instruction >> 12) & 0xf; + const u32 rs = (instruction >> 8) & 0xf; + const u32 rm = instruction & 0xf; + + u64 result; + if (signed_multiply) + { + result = static_cast(static_cast(static_cast(ReadRegisterOperand(rm))) * + static_cast(static_cast(ReadRegisterOperand(rs)))); + } + else + { + result = static_cast(ReadRegisterOperand(rm)) * ReadRegisterOperand(rs); + } + if (accumulate) + result += (static_cast(m_registers[rd_hi]) << 32) | m_registers[rd_lo]; + m_registers[rd_lo] = static_cast(result); + m_registers[rd_hi] = static_cast(result >> 32); + if (set_flags) + { + m_cpsr &= ~(CPSR_N | CPSR_Z); + if (result & (1ULL << 63)) + m_cpsr |= CPSR_N; + if (result == 0) + m_cpsr |= CPSR_Z; + } +} + +void ARMCore::ExecuteBranch(u32 instruction) +{ + const bool link = (instruction & (1U << 24)) != 0; + const s32 offset = SignExtend((instruction & 0x00ffffff) << 2, 26); + if (link) + m_registers[14] = m_instruction_address + 4; + WritePC(m_instruction_address + 8 + static_cast(offset)); +} + +void ARMCore::ExecuteCoprocessor(u32 instruction) +{ + const bool load = (instruction & (1U << 20)) != 0; + const u32 opcode1 = (instruction >> 21) & 7; + const u32 crn = (instruction >> 16) & 0xf; + const u32 rd = (instruction >> 12) & 0xf; + const u32 coprocessor = (instruction >> 8) & 0xf; + const u32 opcode2 = (instruction >> 5) & 7; + const u32 crm = instruction & 0xf; + if (coprocessor != 15) + { + UndefinedInstruction(instruction); + return; + } + + if (load) + { + const u32 value = ReadCP15(opcode1, crn, crm, opcode2); + if (rd == 15) + m_cpsr = (m_cpsr & 0x0fffffff) | (value & 0xf0000000); + else + m_registers[rd] = value; + } + else + { + WriteCP15(opcode1, crn, crm, opcode2, ReadRegisterOperand(rd)); + } +} + +void ARMCore::ExecutePSRTransfer(u32 instruction) +{ + if ((instruction & 0x0fbf0fff) == 0x010f0000) // MRS + { + const bool spsr = (instruction & (1U << 22)) != 0; + const u32 rd = (instruction >> 12) & 0xf; + const u32* source = spsr ? GetSPSR(GetMode()) : &m_cpsr; + m_registers[rd] = source ? *source : m_cpsr; + return; + } + + const bool spsr = (instruction & (1U << 22)) != 0; + const u32 field_mask = (instruction >> 16) & 0xf; + u32 value; + if (instruction & (1U << 25)) + { + const u32 rotate = ((instruction >> 8) & 0xf) * 2; + value = std::rotr(instruction & 0xff, static_cast(rotate)); + } + else + { + value = ReadRegisterOperand(instruction & 0xf); + } + WritePSR(spsr, field_mask, value); +} + +void ARMCore::EnterException(Mode mode, u32 vector, u32 return_address, bool disable_fiq) +{ + const u32 old_cpsr = m_cpsr; + SwitchMode(mode); + if (u32* spsr = GetSPSR(mode)) + *spsr = old_cpsr; + m_cpsr = (old_cpsr & ~(CPSR_MODE_MASK | CPSR_T)) | ModeValue(mode) | CPSR_I; + if (disable_fiq) + m_cpsr |= CPSR_F; + m_registers[14] = return_address; + m_registers[15] = vector & ~3U; + m_pc_written = true; +} + +void ARMCore::UndefinedInstruction(u32 instruction) +{ + m_last_undefined_instruction = instruction; + const u32 return_address = m_instruction_address + ((m_cpsr & CPSR_T) ? 2 : 4); + EnterException(Mode::Undefined, STARLET_EXCEPTION_VECTOR_BASE + 0x04, return_address); +} + +void ARMCore::SaveBankedRegisters(Mode mode) +{ + if (mode == Mode::FIQ) + { + for (size_t i = 0; i < 7; ++i) + m_fiq_r8_r14[i] = m_registers[8 + i]; + return; + } + + for (size_t i = 0; i < 5; ++i) + m_user_r8_r14[i] = m_registers[8 + i]; + std::array* bank = nullptr; + switch (mode) + { + case Mode::User: + case Mode::System: + m_user_r8_r14[5] = m_registers[13]; + m_user_r8_r14[6] = m_registers[14]; + return; + case Mode::IRQ: + bank = &m_irq_r13_r14; + break; + case Mode::Supervisor: + bank = &m_svc_r13_r14; + break; + case Mode::Abort: + bank = &m_abt_r13_r14; + break; + case Mode::Undefined: + bank = &m_und_r13_r14; + break; + default: + return; + } + (*bank)[0] = m_registers[13]; + (*bank)[1] = m_registers[14]; +} + +void ARMCore::LoadBankedRegisters(Mode mode) +{ + if (mode == Mode::FIQ) + { + for (size_t i = 0; i < 7; ++i) + m_registers[8 + i] = m_fiq_r8_r14[i]; + return; + } + + for (size_t i = 0; i < 5; ++i) + m_registers[8 + i] = m_user_r8_r14[i]; + const std::array* bank = nullptr; + switch (mode) + { + case Mode::User: + case Mode::System: + m_registers[13] = m_user_r8_r14[5]; + m_registers[14] = m_user_r8_r14[6]; + return; + case Mode::IRQ: + bank = &m_irq_r13_r14; + break; + case Mode::Supervisor: + bank = &m_svc_r13_r14; + break; + case Mode::Abort: + bank = &m_abt_r13_r14; + break; + case Mode::Undefined: + bank = &m_und_r13_r14; + break; + default: + return; + } + m_registers[13] = (*bank)[0]; + m_registers[14] = (*bank)[1]; +} + +void ARMCore::SwitchMode(Mode mode) +{ + const Mode old_mode = GetMode(); + if (old_mode == mode) + return; + SaveBankedRegisters(old_mode); + m_cpsr = (m_cpsr & ~CPSR_MODE_MASK) | ModeValue(mode); + LoadBankedRegisters(mode); +} + +u32* ARMCore::GetSPSR(Mode mode) +{ + switch (mode) + { + case Mode::FIQ: + return &m_spsr_fiq; + case Mode::IRQ: + return &m_spsr_irq; + case Mode::Supervisor: + return &m_spsr_svc; + case Mode::Abort: + return &m_spsr_abt; + case Mode::Undefined: + return &m_spsr_und; + default: + return nullptr; + } +} + +const u32* ARMCore::GetSPSR(Mode mode) const +{ + return const_cast(this)->GetSPSR(mode); +} + +void ARMCore::RestoreCPSRFromSPSR() +{ + if (const u32* spsr = GetSPSR(GetMode())) + SetCPSR(*spsr); +} + +void ARMCore::WritePSR(bool spsr, u32 field_mask, u32 value) +{ + u32 mask = 0; + if (field_mask & 1) + mask |= 0x000000ff; + if (field_mask & 2) + mask |= 0x0000ff00; + if (field_mask & 4) + mask |= 0x00ff0000; + if (field_mask & 8) + mask |= 0xff000000; + + if (spsr) + { + if (u32* destination = GetSPSR(GetMode())) + *destination = (*destination & ~mask) | (value & mask); + return; + } + + if (GetMode() == Mode::User) + mask &= 0xff000000; + SetCPSR((m_cpsr & ~mask) | (value & mask)); +} + +u32 ARMCore::ReadCP15(u32 opcode1, u32 crn, u32 crm, u32 opcode2) const +{ + if (opcode1 == 0 && crn == 0 && crm == 0 && opcode2 == 0) + return 0x41069265; // ARM926EJ-S main ID register. + if (opcode1 == 0 && crn == 7 && (crm == 10 || crm == 14) && opcode2 == 3) + { + // ARM926 test-and-clean operations return the D-cache dirty state in + // bit 30. Caches are not modeled, so memory is already coherent and the + // cache must always report clean. + return 1U << 30; + } + if (opcode1 != 0) + return 0; + switch (crn) + { + case 1: + return m_cp15.control; + case 2: + return m_cp15.translation_table_base; + case 3: + return m_cp15.domain_access_control; + case 5: + return m_cp15.fault_status; + case 6: + return m_cp15.fault_address; + case 13: + return m_cp15.process_id; + default: + return 0; + } +} + +void ARMCore::WriteCP15(u32 opcode1, u32 crn, u32 crm, u32 opcode2, u32 value) +{ + if (opcode1 != 0) + return; + + // ARM926EJ-S wait for interrupt: MCR p15, 0, Rd, c7, c0, 4. If an interrupt + // is already asserted the instruction completes without entering the + // low-power state. + if (crn == 7 && crm == 0 && opcode2 == 4) + { + m_waiting_for_interrupt = !m_irq_line && !m_fiq_line; + return; + } + + switch (crn) + { + case 1: + m_cp15.control = value; + m_big_endian = (value & CP15_CONTROL_BIG_ENDIAN) != 0; + InvalidateTLB(); + InvalidateInstructionCache(); + break; + case 2: + m_cp15.translation_table_base = value; + InvalidateTLB(); + break; + case 3: + m_cp15.domain_access_control = value; + InvalidateTLB(); + break; + case 5: + m_cp15.fault_status = value; + break; + case 6: + m_cp15.fault_address = value; + break; + case 7: // Treat cache maintenance conservatively as a whole instruction-cache + // invalidation. + InvalidateInstructionCache(); + break; + case 8: // TLB maintenance. + InvalidateTLB(); + break; + case 13: + m_cp15.process_id = value; + InvalidateTLB(); + break; + default: + break; + } +} +} // namespace IOS::LLE diff --git a/Source/Core/Core/IOS/Starlet/ARMCore.h b/Source/Core/Core/IOS/Starlet/ARMCore.h new file mode 100644 index 0000000000..553282e7d7 --- /dev/null +++ b/Source/Core/Core/IOS/Starlet/ARMCore.h @@ -0,0 +1,260 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#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((static_cast(Read8(address)) << 8) | Read8(address + 1)); + } + virtual u32 Read32(u32 address) + { + return (static_cast(Read8(address)) << 24) | (static_cast(Read8(address + 1)) << 16) | + (static_cast(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(value >> 8)); + Write8(address + 1, static_cast(value)); + } + virtual void Write32(u32 address, u32 value) + { + Write8(address, static_cast(value >> 24)); + Write8(address + 1, static_cast(value >> 16)); + Write8(address + 2, static_cast(value >> 8)); + Write8(address + 3, static_cast(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 m_registers{}; + u32 m_cpsr = 0; + + // User/System bank, FIQ bank, and r13/r14 banks for the remaining privileged + // modes. + std::array m_user_r8_r14{}; + std::array m_fiq_r8_r14{}; + std::array m_irq_r13_r14{}; + std::array m_svc_r13_r14{}; + std::array m_abt_r13_r14{}; + std::array 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 m_tlb{}; + std::array 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 diff --git a/Source/Core/Core/IOS/Starlet/Starlet.cpp b/Source/Core/Core/IOS/Starlet/Starlet.cpp new file mode 100644 index 0000000000..315f679b19 --- /dev/null +++ b/Source/Core/Core/IOS/Starlet/Starlet.cpp @@ -0,0 +1,153 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/IOS/Starlet/Starlet.h" + +#include +#include + +#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(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(*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 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(1, static_cast(broadway_cycles) - cycles_late); + m_system.GetCoreTiming().ScheduleEvent(next, m_run_event); +} +} // namespace IOS::LLE diff --git a/Source/Core/Core/IOS/Starlet/Starlet.h b/Source/Core/Core/IOS/Starlet/Starlet.h new file mode 100644 index 0000000000..3bb70a6cbf --- /dev/null +++ b/Source/Core/Core/IOS/Starlet/Starlet.h @@ -0,0 +1,84 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#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 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 m_memory; + std::unique_ptr m_core; + CoreTiming::EventType* m_run_event = nullptr; + bool m_initialized = false; +}; +} // namespace IOS::LLE diff --git a/Source/Core/Core/IOS/Starlet/StarletMemory.cpp b/Source/Core/Core/IOS/Starlet/StarletMemory.cpp new file mode 100644 index 0000000000..fb6ba488c5 --- /dev/null +++ b/Source/Core/Core/IOS/Starlet/StarletMemory.cpp @@ -0,0 +1,4297 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/IOS/Starlet/StarletMemory.h" + +#include +#include +#include +#include +#include + +#include "Common/ChunkFile.h" +#include "Common/Crypto/AES.h" +#include "Common/Crypto/HMAC.h" +#include "Common/FileUtil.h" +#include "Common/Logging/Log.h" +#include "Common/SDCardUtil.h" +#include "Common/StringUtil.h" +#include "Core/Config/MainSettings.h" +#include "Core/HW/MMIO.h" +#include "Core/HW/Memmap.h" +#include "Core/HW/WII_IPC.h" +#include "Core/HW/Wiimote.h" +#include "Core/HW/WiimoteEmu/DesiredWiimoteState.h" +#include "Core/IOS/Network/MACUtils.h" +#include "Core/IOS/Starlet/Starlet.h" +#include "Core/IOS/USB/Bluetooth/BTBase.h" +#include "Core/IOS/USB/Bluetooth/hci.h" +#include "Core/System.h" +#include "DiscIO/NANDImporter.h" +#include "InputCommon/ControllerInterface/ControllerInterface.h" + +namespace IOS::LLE +{ +namespace +{ +constexpr u32 NAND_BASE = 0x0d010000; +constexpr u32 NAND_CTRL = NAND_BASE + 0x00; +constexpr u32 NAND_ADDR1 = NAND_BASE + 0x08; +constexpr u32 NAND_ADDR2 = NAND_BASE + 0x0c; +constexpr u32 NAND_DATA = NAND_BASE + 0x10; +constexpr u32 NAND_ECC = NAND_BASE + 0x14; + +constexpr u32 AES_BASE = 0x0d020000; +constexpr u32 AES_CMD = AES_BASE + 0x00; +constexpr u32 AES_SRC = AES_BASE + 0x04; +constexpr u32 AES_DEST = AES_BASE + 0x08; +constexpr u32 AES_KEY = AES_BASE + 0x0c; +constexpr u32 AES_IV = AES_BASE + 0x10; +constexpr u32 AES_CTRL_EXEC = 1U << 31; +constexpr u32 AES_CTRL_IRQ = 1U << 30; +constexpr u32 AES_CTRL_ENABLE = 1U << 28; +constexpr u32 AES_CTRL_DECRYPT = 1U << 27; + +constexpr u32 DI_BASE = 0x0d806000; +constexpr u32 DI_SIZE = 0x28; + +constexpr bool IsDIAddress(u32 address) +{ + return address >= DI_BASE && address < DI_BASE + DI_SIZE; +} + +constexpr u32 SHA_BASE = 0x0d030000; +constexpr u32 SHA_CMD = SHA_BASE + 0x00; +constexpr u32 SHA_SRC = SHA_BASE + 0x04; +constexpr u32 SHA_H0 = SHA_BASE + 0x08; +constexpr u32 SHA_H4 = SHA_BASE + 0x18; + +constexpr std::array OHCI_BASES = {0x0d050000, 0x0d060000}; +constexpr u32 OHCI_SIZE = 0x200; + +constexpr u32 EHCI_BASE = 0x0d040000; +constexpr u32 EHCI_SIZE = 0x100; +constexpr u32 EHCI_CAPLENVER = 0x00; +constexpr u32 EHCI_HCSPARAMS = 0x04; +constexpr u32 EHCI_HCCPARAMS = 0x08; +constexpr u32 EHCI_PORT_ROUTE = 0x0c; +constexpr u32 EHCI_USB_COMMAND = 0x10; +constexpr u32 EHCI_USB_STATUS = 0x14; +constexpr u32 EHCI_USB_INTERRUPT = 0x18; +constexpr u32 EHCI_FRAME_INDEX = 0x1c; +constexpr u32 EHCI_CONTROL_DATA_SEGMENT = 0x20; +constexpr u32 EHCI_PERIODIC_LIST_BASE = 0x24; +constexpr u32 EHCI_ASYNC_LIST_ADDRESS = 0x28; +constexpr u32 EHCI_CONFIGURED_FLAG = 0x50; +constexpr u32 EHCI_PORT_STATUS_1 = 0x54; +constexpr u32 EHCI_PORT_STATUS_2 = 0x58; +constexpr u32 EHCI_COMMAND_RUN = 1U << 0; +constexpr u32 EHCI_COMMAND_RESET = 1U << 1; +constexpr u32 EHCI_COMMAND_PERIODIC_ENABLE = 1U << 4; +constexpr u32 EHCI_COMMAND_ASYNC_ENABLE = 1U << 5; +constexpr u32 EHCI_COMMAND_ASYNC_ADVANCE = 1U << 6; +// Hollywood's EHCI USBCMD reset image contains this implementation-specific +// bit. IOS58 verifies the complete reset image before bringing the controller +// online; returning the generic EHCI value of zero makes its original EHC +// module abort initialization. +constexpr u32 EHCI_COMMAND_HOLLYWOOD_RESET_VALUE = 1U << 19; +constexpr u32 EHCI_STATUS_USB_INTERRUPT = 1U << 0; +constexpr u32 EHCI_STATUS_USB_ERROR = 1U << 1; +constexpr u32 EHCI_STATUS_PORT_CHANGE = 1U << 2; +constexpr u32 EHCI_STATUS_FRAME_ROLLOVER = 1U << 3; +constexpr u32 EHCI_STATUS_HOST_SYSTEM_ERROR = 1U << 4; +constexpr u32 EHCI_STATUS_ASYNC_ADVANCE = 1U << 5; +constexpr u32 EHCI_STATUS_HALTED = 1U << 12; +constexpr u32 EHCI_STATUS_PERIODIC_ACTIVE = 1U << 14; +constexpr u32 EHCI_STATUS_ASYNC_ACTIVE = 1U << 15; +constexpr u32 EHCI_INTERRUPT_MASK = EHCI_STATUS_USB_INTERRUPT | EHCI_STATUS_USB_ERROR | + EHCI_STATUS_PORT_CHANGE | EHCI_STATUS_FRAME_ROLLOVER | + EHCI_STATUS_HOST_SYSTEM_ERROR | EHCI_STATUS_ASYNC_ADVANCE; +constexpr u32 EHCI_PORT_CONNECT_STATUS = 1U << 0; +constexpr u32 EHCI_PORT_CONNECT_CHANGE = 1U << 1; +constexpr u32 EHCI_PORT_ENABLE = 1U << 2; +constexpr u32 EHCI_PORT_ENABLE_CHANGE = 1U << 3; +constexpr u32 EHCI_PORT_OVER_CURRENT_CHANGE = 1U << 5; +constexpr u32 EHCI_PORT_SUSPEND = 1U << 7; +constexpr u32 EHCI_PORT_RESET = 1U << 8; +constexpr u32 EHCI_PORT_POWER = 1U << 12; +constexpr u32 EHCI_PORT_OWNER = 1U << 13; +constexpr u32 EHCI_PORT_WRITE_CLEAR_MASK = + EHCI_PORT_CONNECT_CHANGE | EHCI_PORT_ENABLE_CHANGE | EHCI_PORT_OVER_CURRENT_CHANGE; +constexpr u32 EHCI_MICROFRAME_CYCLES = 30'375; +constexpr u32 EHCI_OPERATIONAL_RESET_END = 0x80; +constexpr u32 OHCI_REVISION = 0x00; +constexpr u32 OHCI_CONTROL = 0x04; +constexpr u32 OHCI_COMMAND_STATUS = 0x08; +constexpr u32 OHCI_INTERRUPT_STATUS = 0x0c; +constexpr u32 OHCI_INTERRUPT_ENABLE = 0x10; +constexpr u32 OHCI_INTERRUPT_DISABLE = 0x14; +constexpr u32 OHCI_HCCA = 0x18; +constexpr u32 OHCI_PERIOD_CURRENT_ED = 0x1c; +constexpr u32 OHCI_CONTROL_HEAD_ED = 0x20; +constexpr u32 OHCI_CONTROL_CURRENT_ED = 0x24; +constexpr u32 OHCI_BULK_HEAD_ED = 0x28; +constexpr u32 OHCI_BULK_CURRENT_ED = 0x2c; +constexpr u32 OHCI_DONE_HEAD = 0x30; +constexpr u32 OHCI_FRAME_INTERVAL = 0x34; +constexpr u32 OHCI_FRAME_REMAINING = 0x38; +constexpr u32 OHCI_FRAME_NUMBER = 0x3c; +constexpr u32 OHCI_PERIODIC_START = 0x40; +constexpr u32 OHCI_LOW_SPEED_THRESHOLD = 0x44; +constexpr u32 OHCI_RH_DESCRIPTOR_A = 0x48; +constexpr u32 OHCI_RH_DESCRIPTOR_B = 0x4c; +constexpr u32 OHCI_RH_STATUS = 0x50; +constexpr u32 OHCI_RH_PORT_STATUS_1 = 0x54; +constexpr u32 OHCI_RH_PORT_STATUS_2 = 0x58; + +constexpr u32 OHCI_CONTROL_FUNCTIONAL_STATE_MASK = 3U << 6; +constexpr u32 OHCI_CONTROL_FUNCTIONAL_STATE_OPERATIONAL = 2U << 6; +constexpr u32 OHCI_COMMAND_HOST_CONTROLLER_RESET = 1U << 0; +constexpr u32 OHCI_COMMAND_CONTROL_LIST_FILLED = 1U << 1; +constexpr u32 OHCI_COMMAND_BULK_LIST_FILLED = 1U << 2; +constexpr u32 OHCI_CONTROL_PERIODIC_LIST_ENABLE = 1U << 2; +constexpr u32 OHCI_CONTROL_CONTROL_LIST_ENABLE = 1U << 4; +constexpr u32 OHCI_CONTROL_BULK_LIST_ENABLE = 1U << 5; +constexpr u32 OHCI_INTERRUPT_WRITEBACK_DONE_HEAD = 1U << 1; +constexpr u32 OHCI_INTERRUPT_START_OF_FRAME = 1U << 2; +constexpr u32 OHCI_INTERRUPT_FRAME_NUMBER_OVERFLOW = 1U << 5; +constexpr u32 OHCI_INTERRUPT_ROOT_HUB_STATUS_CHANGE = 1U << 6; +constexpr u32 OHCI_INTERRUPT_MASTER_ENABLE = 1U << 31; +constexpr u32 OHCI_PORT_CURRENT_CONNECT_STATUS = 1U << 0; +constexpr u32 OHCI_PORT_ENABLE_STATUS = 1U << 1; +constexpr u32 OHCI_PORT_SUSPEND_STATUS = 1U << 2; +constexpr u32 OHCI_PORT_RESET_STATUS = 1U << 4; +constexpr u32 OHCI_PORT_POWER_STATUS = 1U << 8; +constexpr u32 OHCI_PORT_CONNECT_STATUS_CHANGE = 1U << 16; +constexpr u32 OHCI_PORT_ENABLE_STATUS_CHANGE = 1U << 17; +constexpr u32 OHCI_PORT_SUSPEND_STATUS_CHANGE = 1U << 18; +constexpr u32 OHCI_PORT_RESET_STATUS_CHANGE = 1U << 20; +constexpr u32 OHCI_PORT_CHANGE_MASK = 0x001f0000; +constexpr u32 OHCI_FRAME_CYCLES = 243000; +constexpr u16 OHCI1_ATTACH_DELAY_FRAMES = 100; +constexpr u64 WIIMOTE_UPDATE_CYCLES = 243000000 / Wiimote::UPDATE_FREQ; +// Poll host controls at Dolphin's normal 200 Hz, but do not wake the +// interpreted IOS Bluetooth stack for every poll. A 30 Hz steady HID stream +// leaves substantially more host time for Broadway; button transitions bypass +// this throttle below so presses and releases still reach IOS promptly. +constexpr u32 WIIMOTE_REPORT_FREQUENCY = 30; +constexpr u64 WIIMOTE_REPORT_CYCLES = 243000000 / WIIMOTE_REPORT_FREQUENCY; +static_assert(WIIMOTE_REPORT_FREQUENCY <= Wiimote::UPDATE_FREQ); +// Hollywood completes the internal OHCI1 port reset before IOS's first 2 ms +// poll. Using the generic 10 ms upper-bound timing leaves IOS with RHSC masked +// when PRSC arrives. +constexpr u8 OHCI_PORT_RESET_FRAMES = 2; +constexpr u32 OHCI_DEFAULT_FRAME_INTERVAL = 0x27782edf; +constexpr u32 OHCI_DEFAULT_LOW_SPEED_THRESHOLD = 0x628; +constexpr u32 OHCI_POWER_ON_TO_POWER_GOOD_TIME = 2U << 24; // 2 * 2 ms. +constexpr u32 OHCI_ENDPOINT_SKIP = 1U << 14; +constexpr u32 OHCI_ENDPOINT_ISOCHRONOUS = 1U << 15; +constexpr u32 OHCI_ENDPOINT_HALTED = 1U << 0; +constexpr u32 OHCI_TD_DIRECTION_SHIFT = 19; +constexpr u32 OHCI_TD_INTERRUPT_DELAY_SHIFT = 21; +constexpr u32 OHCI_TD_CONDITION_CODE_SHIFT = 28; +constexpr u32 OHCI_TD_CONDITION_CODE_MASK = 0xfU << OHCI_TD_CONDITION_CODE_SHIFT; +constexpr u32 OHCI_CC_NO_ERROR = 0; +constexpr u32 OHCI_CC_STALL = 4; +constexpr u32 OHCI_CC_DEVICE_NOT_RESPONDING = 5; +constexpr u32 OHCI_DIRECTION_SETUP = 0; +constexpr u32 OHCI_DIRECTION_OUT = 1; +constexpr u32 OHCI_DIRECTION_IN = 2; + +constexpr u8 USB_REQUEST_GET_STATUS = 0; +constexpr u8 USB_REQUEST_SET_ADDRESS = 5; +constexpr u8 USB_REQUEST_GET_DESCRIPTOR = 6; +constexpr u8 USB_REQUEST_GET_CONFIGURATION = 8; +constexpr u8 USB_REQUEST_SET_CONFIGURATION = 9; +constexpr u8 USB_REQUEST_GET_INTERFACE = 10; +constexpr u8 USB_REQUEST_SET_INTERFACE = 11; + +constexpr u32 SDHC_BASE = 0x0d070000; +constexpr u32 SDHC_SIZE = 0x200; +constexpr u32 SDHC_DMA_ADDRESS = SDHC_BASE + 0x00; +constexpr u32 SDHC_BLOCK_SIZE_AND_COUNT = SDHC_BASE + 0x04; +constexpr u32 SDHC_ARGUMENT = SDHC_BASE + 0x08; +constexpr u32 SDHC_TRANSFER_MODE_AND_COMMAND = SDHC_BASE + 0x0c; +constexpr u32 SDHC_RESPONSE = SDHC_BASE + 0x10; +constexpr u32 SDHC_BUFFER_DATA = SDHC_BASE + 0x20; +constexpr u32 SDHC_PRESENT_STATE = SDHC_BASE + 0x24; +constexpr u32 SDHC_CLOCK_CONTROL = SDHC_BASE + 0x2c; +constexpr u32 SDHC_INTERRUPT_STATUS = SDHC_BASE + 0x30; +constexpr u32 SDHC_INTERRUPT_STATUS_ENABLE = SDHC_BASE + 0x34; +constexpr u32 SDHC_INTERRUPT_SIGNAL_ENABLE = SDHC_BASE + 0x38; +constexpr u32 SDHC_CAPABILITIES = SDHC_BASE + 0x40; +constexpr u32 SDHC_MAX_CAPABILITIES = SDHC_BASE + 0x48; +constexpr u32 SDHC_SLOT_AND_VERSION = SDHC_BASE + 0xfc; +constexpr u32 WIFI_SDIO_BASE = 0x0d080000; +constexpr u32 WIFI_SDIO_TRUSTED_BASE = 0x0d880000; +constexpr u32 WIFI_SDIO_SIZE = 0x200; + +bool IsWiFiSDIOAddress(u32 address) +{ + return (address >= WIFI_SDIO_BASE && address < WIFI_SDIO_BASE + WIFI_SDIO_SIZE) || + (address >= WIFI_SDIO_TRUSTED_BASE && address < WIFI_SDIO_TRUSTED_BASE + WIFI_SDIO_SIZE); +} + +u32 GetWiFiSDIOOffset(u32 address) +{ + return address - (address >= WIFI_SDIO_TRUSTED_BASE ? WIFI_SDIO_TRUSTED_BASE : WIFI_SDIO_BASE); +} +constexpr u32 SDHC_CARD_STATE_STABLE = 1U << 17; +constexpr u32 SDHC_CARD_INSERTED = 1U << 16; +constexpr u32 SDHC_CARD_DETECT_PIN_LEVEL = 1U << 18; +constexpr u32 SDHC_WRITE_PROTECT_SWITCH = 1U << 19; +constexpr u32 SDHC_CARD_SIGNAL_LEVELS = 0x01f00000; +constexpr u32 SDHC_INTERNAL_CLOCK_ENABLE = 1U << 0; +constexpr u32 SDHC_INTERNAL_CLOCK_STABLE = 1U << 1; +constexpr u32 SDHC_SOFTWARE_RESET_MASK = 0x07000000; +constexpr u16 SDHC_TRANSFER_DMA_ENABLE = 1U << 0; +constexpr u16 SDHC_TRANSFER_BLOCK_COUNT_ENABLE = 1U << 1; +constexpr u16 SDHC_TRANSFER_READ = 1U << 4; +constexpr u16 SDHC_COMMAND_DATA_PRESENT = 1U << 5; +constexpr u16 SDHC_INTERRUPT_COMMAND_COMPLETE = 1U << 0; +constexpr u16 SDHC_INTERRUPT_TRANSFER_COMPLETE = 1U << 1; +constexpr u16 SDHC_INTERRUPT_BUFFER_WRITE_READY = 1U << 4; +constexpr u16 SDHC_INTERRUPT_BUFFER_READ_READY = 1U << 5; +constexpr u16 SDHC_INTERRUPT_ERROR = 1U << 15; +constexpr u16 SDHC_ERROR_COMMAND_TIMEOUT = 1U << 0; +constexpr u16 SDHC_ERROR_DATA_TIMEOUT = 1U << 4; + +constexpr u32 SD_CARD_GO_IDLE = 0; +constexpr u32 SD_CARD_ALL_SEND_CID = 2; +constexpr u32 SD_CARD_SEND_RELATIVE_ADDRESS = 3; +constexpr u32 SD_CARD_SELECT = 7; +constexpr u32 SD_CARD_SEND_IF_CONDITION = 8; +constexpr u32 SD_CARD_SEND_CSD = 9; +constexpr u32 SD_CARD_SEND_CID = 10; +constexpr u32 SD_CARD_STOP_TRANSMISSION = 12; +constexpr u32 SD_CARD_SEND_STATUS = 13; +constexpr u32 SD_CARD_SET_BLOCK_LENGTH = 16; +constexpr u32 SD_CARD_READ_SINGLE = 17; +constexpr u32 SD_CARD_READ_MULTIPLE = 18; +constexpr u32 SD_CARD_WRITE_SINGLE = 24; +constexpr u32 SD_CARD_WRITE_MULTIPLE = 25; +constexpr u32 SD_CARD_APP_COMMAND = 55; +constexpr u32 SD_CARD_ACMD_SET_BUS_WIDTH = 6; +constexpr u32 SD_CARD_ACMD_SEND_OP_CONDITION = 41; +constexpr u32 SD_CARD_ACMD_SEND_SCR = 51; +constexpr u32 SD_IO_SEND_OP_CONDITION = 5; +constexpr u32 SD_IO_RW_DIRECT = 52; +constexpr u32 SD_IO_RW_EXTENDED = 53; +constexpr u16 SD_CARD_RELATIVE_ADDRESS = 0x9f62; +// Capabilities of the front SD slot as exposed through Hollywood's +// reversed-little-endian SDHCI register window. Card presence is reported +// separately by SDHC_PRESENT_STATE. +constexpr u32 SDHC_CAPABILITIES_VALUE = 0x01603201; +// The soldered-down BCM4318 uses a distinct host controller. IOS's WL driver +// reads its 48 MHz base clock from the low six bits while retaining the +// voltage, SDMA and high-speed flags. +constexpr u32 WIFI_SDIO_CAPABILITIES_VALUE = (1U << 24) | (1U << 22) | (1U << 21) | 48U; +// Hollywood exposes one SDHCI slot (bit 0) followed by an SD Host Controller +// Specification 1.0, vendor revision 0 version field. Its 32-bit data-invariant +// bus therefore returns 0x00000001 for the combined register at offset 0xfc. +constexpr u32 SDHC_VERSION_VALUE = 0x00000001; + +constexpr u32 MEM_AHMFLUSH = 0x0d8b4228; +constexpr u32 MEM_AHMFLUSH_ACK = 0x0d8b422a; +constexpr u32 MEM_DDRREG_ADDR = 0x0d8b4074; +constexpr u32 MEM_DDRREG_DATA = 0x0d8b4076; +constexpr u32 MEM_SEQ_DATA = 0x0d8b42c4; +constexpr u32 MEM_SEQ_ADDR = 0x0d8b42c6; +constexpr u32 MEM_BIST_DATA = 0x0d8b42c8; +constexpr u32 MEM_BIST_ADDR = 0x0d8b42ca; + +constexpr u16 DDR_AHMFLUSH = 0x114; +constexpr u16 DDR_AHMFLUSH_ACK = 0x115; +constexpr u16 DDR_SEQ_DATA = 0x162; +constexpr u16 DDR_SEQ_ADDR = 0x163; +constexpr u16 DDR_BIST_DATA = 0x164; +constexpr u16 DDR_BIST_ADDR = 0x165; + +constexpr u16 BIST_WRGO = 0x01; +constexpr u16 BIST_RDGO = 0x05; + +constexpr u32 HW_BASE = 0x0d800000; +constexpr u32 HW_USBFRCRST = HW_BASE + 0x88; +constexpr u32 HW_SRNPROT = HW_BASE + 0x60; +constexpr u32 HW_AHBPROT = HW_BASE + 0x64; +constexpr u32 HW_TIMER = HW_BASE + 0x10; +constexpr u32 HW_ALARM = HW_BASE + 0x14; +constexpr u32 HW_GPIO_OUT = HW_BASE + 0xe0; +constexpr u32 HW_GPIO_IN = HW_BASE + 0xe8; +constexpr u32 HW_DIFLAGS = HW_BASE + 0x180; +constexpr u32 HW_SPARE0 = HW_BASE + 0x188; +constexpr u32 HW_BOOT0 = HW_BASE + 0x18c; +constexpr u32 HW_RESETS = HW_BASE + 0x194; +constexpr u32 HW_IFPOWER = HW_BASE + 0x198; +constexpr u32 HW_IOPWRCTRL = HW_BASE + 0x1dc; +constexpr u32 HW_OTPCMD = HW_BASE + 0x1ec; +constexpr u32 HW_OTPDATA = HW_BASE + 0x1f0; +constexpr u32 HW_VERSION = HW_BASE + 0x214; + +constexpr u32 DIFLAGS_BOOT_CODE = 1U << 20; +constexpr u32 EXI_BOOT_BASE = 0x0d806840; +constexpr u32 BROADWAY_RESET_VECTOR_BASE = 0xfff00100; +constexpr u32 BROADWAY_RESET_VECTOR_SIZE = 0x40; + +constexpr u32 NAND_CTRL_EXEC = 1U << 31; +constexpr u32 NAND_CTRL_ACK = ~NAND_CTRL_EXEC; +constexpr u32 NAND_CTRL_IRQ = 1U << 30; +constexpr u32 NAND_CTRL_ERROR = 1U << 29; +constexpr u32 NAND_CTRL_WRITE = 1U << 14; +constexpr u32 NAND_CTRL_READ = 1U << 13; +constexpr u32 NAND_CTRL_ECC = 1U << 12; +constexpr u32 NAND_CTRL_DATA_LENGTH_MASK = 0xfff; +constexpr u8 NAND_CMD_READ_PRE = 0x00; +constexpr u8 NAND_CMD_RANDOM_OUTPUT = 0x05; +constexpr u8 NAND_CMD_PROGRAM_CONFIRM = 0x10; +constexpr u8 NAND_CMD_READ_CONFIRM = 0x30; +constexpr u8 NAND_CMD_READ_SPARE = 0x50; +constexpr u8 NAND_CMD_ERASE_PRE = 0x60; +constexpr u8 NAND_CMD_STATUS = 0x70; +constexpr u8 NAND_CMD_STATUS_MULTI = 0x71; +constexpr u8 NAND_CMD_PROGRAM_PRE = 0x80; +constexpr u8 NAND_CMD_RANDOM_INPUT = 0x85; +constexpr u8 NAND_CMD_READ_ID = 0x90; +constexpr u8 NAND_CMD_ERASE_CONFIRM = 0xd0; +constexpr u8 NAND_CMD_RANDOM_OUTPUT_CONFIRM = 0xe0; +constexpr u8 NAND_CMD_RESET = 0xff; + +constexpr std::array NAND_CHIP_ID = {0xec, 0xdc, 0x10, 0x95, 0x54}; +constexpr u8 NAND_STATUS_READY = 1U << 6; +constexpr u8 NAND_STATUS_NOT_WRITE_PROTECTED = 1U << 7; +constexpr u8 NAND_STATUS_FAIL = 1U << 0; + +constexpr u64 NAND_IMAGE_SIZE = 0x21000000; +constexpr u64 BOOTMII_NAND_IMAGE_SIZE = NAND_IMAGE_SIZE + 0x400; +constexpr size_t BOOT1_HASH_OFFSET_IN_KEYS = 0x100; +constexpr size_t NAND_HMAC_KEY_OFFSET_IN_KEYS = 0x144; +constexpr size_t NAND_AES_KEY_OFFSET_IN_KEYS = 0x158; +constexpr size_t SEEPROM_OFFSET_IN_KEYS = 0x200; +constexpr size_t OTP_SIZE = 0x80; +constexpr size_t SEEPROM_SIZE = 0x100; + +constexpr u32 GPIO_EEP_CS = 0x400; +constexpr u32 GPIO_EEP_CLK = 0x800; +constexpr u32 GPIO_EEP_MOSI = 0x1000; +constexpr u32 GPIO_EEP_MISO = 0x2000; + +constexpr u32 SRNPROT_SRAM_SPLIT_MODE = 1U << 5; +constexpr u32 BOOT0_DISABLE = 1U << 12; +constexpr u32 SRAM_A_SIZE = 0x10000; +constexpr u32 SRAM_B_SIZE = 0x8000; +constexpr u32 INVALID_SRAM_OFFSET = 0xffffffff; + +std::string PathInDirectory(const std::string& directory, const char* filename) +{ + if (directory.empty()) + return {}; + const char last = directory.back(); + return directory + (last == '/' || last == '\\' ? "" : "/") + filename; +} +} // namespace + +StarletMemory::StarletMemory(Core::System& system) : m_system(system) +{ +} + +bool StarletMemory::Init(const std::string& dump_directory, std::string* error) +{ + const std::string boot_path = PathInDirectory(dump_directory, "boot0.bin"); + const std::string keys_path = PathInDirectory(dump_directory, "keys.bin"); + const std::string nand_path = PathInDirectory(dump_directory, "nand.bin"); + if (boot_path.empty()) + { + if (error) + *error = "WiiIOSLLEDumpPath is empty"; + return false; + } + + File::IOFile boot_file(boot_path, "rb"); + File::IOFile keys_file(keys_path, "rb"); + m_nand = File::IOFile(nand_path, "rb", File::SharedAccess::Read); + if (!boot_file || boot_file.GetSize() != m_boot_rom.size() || !boot_file.ReadArray(&m_boot_rom)) + { + if (error) + *error = "boot0.bin must be an 8192-byte Wii boot ROM"; + return false; + } + if (!keys_file || keys_file.GetSize() != m_keys.size() || !keys_file.ReadArray(&m_keys)) + { + if (error) + *error = "keys.bin must be a 1024-byte BootMii key dump"; + return false; + } + const u64 nand_size = m_nand ? m_nand.GetSize() : 0; + if (nand_size != NAND_IMAGE_SIZE && nand_size != BOOTMII_NAND_IMAGE_SIZE) + { + if (error) + *error = "nand.bin must be a 0x21000000-byte raw NAND image or a " + "0x21000400-byte BootMii dump"; + return false; + } + + // Recent BootMii dumps append a keys.bin-shaped footer. The NAND controller + // must never expose that footer as flash pages, but it gives us a safe way to + // reject dumps from a different console. Ignore the human-readable header + // because it can differ between backup sessions. + if (nand_size == BOOTMII_NAND_IMAGE_SIZE) + { + std::array footer{}; + if (!m_nand.Seek(NAND_IMAGE_SIZE, File::SeekOrigin::Begin) || !m_nand.ReadArray(&footer) || + !std::equal(footer.begin() + BOOT1_HASH_OFFSET_IN_KEYS, + footer.begin() + BOOT1_HASH_OFFSET_IN_KEYS + OTP_SIZE, + m_keys.begin() + BOOT1_HASH_OFFSET_IN_KEYS) || + !std::equal(footer.begin() + SEEPROM_OFFSET_IN_KEYS, + footer.begin() + SEEPROM_OFFSET_IN_KEYS + SEEPROM_SIZE, + m_keys.begin() + SEEPROM_OFFSET_IN_KEYS)) + { + if (error) + *error = "the NAND BootMii footer does not match keys.bin"; + return false; + } + } + + // Pairing identities must be selected before WiimoteDevice installs callbacks + // on the input sources. Constructing the synthetic devices in Reset and + // replacing them immediately after reading BT.DINF leaves a short-lived + // callback pointing at a destroyed device. + m_initialized = false; + InitSDCard(); + Reset(); + if (!InstallEmulatedWiimotePairings()) + { + if (error) + *error = "could not install emulated Wii Remote pairings in the raw NAND " + "overlay"; + m_initialized = false; + return false; + } + m_initialized = true; + InitializeWiimotes(); + return true; +} + +void StarletMemory::InitSDCard() +{ + m_sd_card.Close(); + m_sd_card_inserted = false; + m_sd_card_high_capacity = false; + m_sd_block_length = 512; + if (!Config::Get(Config::MAIN_WII_SD_CARD)) + return; + + const std::string filename = File::GetUserPath(F_WIISDCARDIMAGE_IDX); + m_sd_card.Open(filename, "r+b"); + if (!m_sd_card) + { + WARN_LOG_FMT(IOS_SD, "Could not open the LLE SD image; creating a 128 MiB card at {}", + filename); + if (Common::SDCardCreate(128, filename)) + m_sd_card.Open(filename, "r+b"); + } + if (!m_sd_card) + { + ERROR_LOG_FMT(IOS_SD, "Wii IOS LLE could not open or create the configured SD image"); + return; + } + + m_sd_card_inserted = true; + m_sd_card_high_capacity = m_sd_card.GetSize() > 0x80000000ULL; +} + +void StarletMemory::Reset() +{ + for (auto& wiimote : m_wiimotes) + { + if (wiimote) + wiimote->SetSource(nullptr); + wiimote.reset(); + } + m_sram.fill(0); + m_registers.clear(); + m_nand_overlay.clear(); + m_nand_control_before_write = 0; + ResetNANDOperationState(); + m_aes_key.fill(0); + m_aes_iv.fill(0); + m_sha_state.fill(0); + m_ddr_registers.fill(0); + m_ddr_seq_registers.fill(0); + m_ddr_bist_registers.fill(0); + std::copy_n(m_keys.begin() + SEEPROM_OFFSET_IN_KEYS, SEEPROM_SIZE, m_seeprom.begin()); + m_ddr_register_address = 0; + m_ddr_seq_address = 0; + m_ddr_bist_address = 0; + m_gpio_out = 0; + m_seeprom_command = 0; + m_seeprom_output = 0; + m_seeprom_input = 0; + m_seeprom_command_bits = 0; + m_seeprom_output_bits = 0; + m_seeprom_input_bits = 0; + m_seeprom_write_address = 0; + m_seeprom_miso = false; + m_seeprom_write_enabled = false; + m_seeprom_write_pending = false; + m_seeprom_write_all = false; + m_sd_app_command = false; + m_sd_card_selected = false; + m_sd_block_length = 512; + m_sdhc_status_before_write = 0; + m_wifi_sdio_status_before_write = 0; + m_wifi_sdio_registers.clear(); + m_wifi_sdio_pio_read_data.clear(); + m_wifi_sdio_pio_write_function = 0; + m_wifi_sdio_pio_write_address = 0; + m_wifi_sdio_pio_write_remaining = 0; + m_wifi_sdio_pio_write_increment = false; + m_wifi_sdio_selected = false; + m_ehci_interrupt_enable = 0; + m_ehci_status_before_write = 0; + m_usb_force_reset_before_write = 0; + m_ehci_microframe_cycle_remainder = 0; + m_ehci_running = false; + m_ohci_interrupt_enable.fill(0); + m_ohci_interrupt_status_before_write.fill(0); + m_ohci_port_status_before_write = {}; + m_ohci_port_reset_frames = {}; + m_ohci_frame_cycle_remainder.fill(0); + m_ohci_operational.fill(false); + m_ohci1_attach_delay_frames = 0; + m_ohci1_setup_packet.fill(0); + m_ohci1_control_response.clear(); + m_ohci1_hci_events.clear(); + m_ohci1_acl_packets.clear(); + m_ohci1_acl_delay_frames = 0; + m_wiimote_cycle_remainder = 0; + m_last_wiimote_buttons.fill(0); + m_ohci1_scan_enable = 0; + m_ohci1_device_address = 0; + m_ohci1_pending_address = 0xff; + m_ohci1_configuration = 0; + m_ohci1_control_request_valid = false; + m_ohci1_control_stalled = false; + m_ohci1_hci_command_pending = false; + ResetEHCIController(); + for (size_t controller = 0; controller < OHCI_BASES.size(); ++controller) + ResetOHCIController(controller); + m_arm_cycles = 0; + m_boot0_mapped = true; + m_sram_split_mode = false; + // Retail Hollywood production revision. Early firmware branches on this + // read-only register. + WriteRegister(HW_VERSION, 0x11); + if (m_initialized) + InitializeWiimotes(); +} + +void StarletMemory::DoState(PointerWrap& p) +{ + p.DoArray(m_sram); + p.Do(m_registers); + p.Do(m_nand_overlay); + p.DoArray(m_nand_program_data); + p.Do(m_nand_control_before_write); + p.Do(m_nand_read_page); + p.Do(m_nand_read_column); + p.Do(m_nand_program_page); + p.Do(m_nand_erase_page); + p.Do(m_nand_status); + p.Do(m_nand_read_pending); + p.Do(m_nand_program_pending); + p.Do(m_nand_program_ecc_enabled); + p.Do(m_nand_erase_pending); + p.Do(m_nand_pending_command); + p.Do(m_nand_cycles_until_completion); + p.Do(m_sd_card_inserted); + p.Do(m_sd_card_high_capacity); + p.Do(m_sd_app_command); + p.Do(m_sd_card_selected); + p.Do(m_sd_block_length); + p.Do(m_sdhc_status_before_write); + p.Do(m_wifi_sdio_status_before_write); + p.Do(m_wifi_sdio_registers); + p.Do(m_wifi_sdio_pio_read_data); + p.Do(m_wifi_sdio_pio_write_function); + p.Do(m_wifi_sdio_pio_write_address); + p.Do(m_wifi_sdio_pio_write_remaining); + p.Do(m_wifi_sdio_pio_write_increment); + p.Do(m_wifi_sdio_selected); + p.Do(m_ehci_interrupt_enable); + p.Do(m_ehci_status_before_write); + p.Do(m_usb_force_reset_before_write); + p.Do(m_ehci_microframe_cycle_remainder); + p.Do(m_ehci_running); + p.DoArray(m_ohci_interrupt_enable); + p.DoArray(m_ohci_interrupt_status_before_write); + for (auto& port_status : m_ohci_port_status_before_write) + p.DoArray(port_status); + for (auto& reset_frames : m_ohci_port_reset_frames) + p.DoArray(reset_frames); + p.DoArray(m_ohci_frame_cycle_remainder); + p.DoArray(m_ohci_operational); + p.Do(m_ohci1_attach_delay_frames); + p.DoArray(m_ohci1_setup_packet); + p.Do(m_ohci1_control_response); + p.Do(m_ohci1_hci_events); + p.Do(m_ohci1_acl_packets); + p.Do(m_ohci1_acl_delay_frames); + for (auto& address : m_wiimote_addresses) + p.DoArray(address); + for (auto& wiimote : m_wiimotes) + { + if (wiimote) + wiimote->DoState(p); + } + p.Do(m_wiimote_cycle_remainder); + p.DoArray(m_last_wiimote_buttons); + p.Do(m_ohci1_scan_enable); + p.Do(m_ohci1_device_address); + p.Do(m_ohci1_pending_address); + p.Do(m_ohci1_configuration); + p.Do(m_ohci1_control_request_valid); + p.Do(m_ohci1_control_stalled); + p.Do(m_ohci1_hci_command_pending); + p.DoArray(m_aes_key); + p.DoArray(m_aes_iv); + p.DoArray(m_sha_state); + p.DoArray(m_ddr_registers); + p.DoArray(m_ddr_seq_registers); + p.DoArray(m_ddr_bist_registers); + p.DoArray(m_seeprom); + p.Do(m_ddr_register_address); + p.Do(m_ddr_seq_address); + p.Do(m_ddr_bist_address); + p.Do(m_gpio_out); + p.Do(m_seeprom_command); + p.Do(m_seeprom_output); + p.Do(m_seeprom_input); + p.Do(m_seeprom_command_bits); + p.Do(m_seeprom_output_bits); + p.Do(m_seeprom_input_bits); + p.Do(m_seeprom_write_address); + p.Do(m_seeprom_miso); + p.Do(m_seeprom_write_enabled); + p.Do(m_seeprom_write_pending); + p.Do(m_seeprom_write_all); + p.Do(m_arm_cycles); + p.Do(m_initialized); + p.Do(m_boot0_mapped); + p.Do(m_sram_split_mode); +} + +u32 StarletMemory::GetTimer() const +{ + // Hollywood's 19.2 MHz timer is clocked at 32/405 of the 243 MHz Starlet + // clock. + const u64 timer = (m_arm_cycles / 405) * 32 + ((m_arm_cycles % 405) * 32) / 405; + return static_cast(timer); +} + +std::optional StarletMemory::TryReadBroadwayResetInstruction(u32 address) const +{ + if ((ReadRegister(HW_DIFLAGS) & DIFLAGS_BOOT_CODE) == 0 || address < BROADWAY_RESET_VECTOR_BASE || + address >= BROADWAY_RESET_VECTOR_BASE + BROADWAY_RESET_VECTOR_SIZE || (address & 3) != 0) + { + return std::nullopt; + } + + return ReadRegister(EXI_BOOT_BASE + address - BROADWAY_RESET_VECTOR_BASE); +} + +void StarletMemory::AdvanceCycles(u64 cycles) +{ + const u32 old_timer = GetTimer(); + m_arm_cycles += cycles; + const u32 new_timer = GetTimer(); + const u32 elapsed_timer_ticks = new_timer - old_timer; + const u32 alarm = ReadRegister(HW_ALARM); + const u32 ticks_until_alarm = alarm - old_timer; + if (ticks_until_alarm != 0 && ticks_until_alarm <= elapsed_timer_ticks) + m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_TIMER, true); + + if (m_nand_cycles_until_completion != 0) + { + if (cycles >= m_nand_cycles_until_completion) + { + const u32 command = m_nand_pending_command; + m_nand_pending_command = 0; + m_nand_cycles_until_completion = 0; + CompleteNANDCommand(command); + } + else + { + m_nand_cycles_until_completion -= cycles; + } + } + + AdvanceEHCI(cycles); + for (size_t controller = 0; controller < OHCI_BASES.size(); ++controller) + AdvanceOHCI(controller, cycles); + + m_wiimote_cycle_remainder += cycles; + if (m_initialized && m_wiimote_cycle_remainder >= WIIMOTE_UPDATE_CYCLES) + { + m_wiimote_cycle_remainder %= WIIMOTE_UPDATE_CYCLES; + UpdateWiimotes(); + } +} + +bool StarletMemory::IsIdlePollAddress(u32 address, u32 size) const +{ + if (size == 0 || address > std::numeric_limits::max() - (size - 1)) + return false; + + const u32 last_address = address + size - 1; + return (IsMemoryAddress(address) && IsMemoryAddress(last_address)) || + (IsSRAMWindowAddress(address) && IsSRAMWindowAddress(last_address)); +} + +bool StarletMemory::IsMemoryAddress(u32 address) +{ + return address < Memory::MEM1_SIZE_RETAIL || + (address >= 0x10000000 && address < 0x10000000 + Memory::MEM2_SIZE_RETAIL); +} + +bool StarletMemory::IsSRAMWindowAddress(u32 address) +{ + return (address >= SRAM_BASE && address < SRAM_BASE + SRAM_WINDOW_SIZE) || + address >= SRAM_MIRROR_BASE; +} + +bool StarletMemory::IsBootROMAddress(u32 address) const +{ + if (!m_boot0_mapped || address < 0xfffe0000) + return false; + + const u32 window_offset = address & (SRAM_WINDOW_SIZE - 1); + // With split mode clear, boot0 occupies the upper half of the high 128 KiB + // window. With split mode set, it moves to the lower half so IOS can place + // its high vectors in SRAM A at ffff0000. + return m_sram_split_mode ? window_offset < SRAM_A_SIZE : window_offset >= SRAM_A_SIZE; +} + +u32 StarletMemory::GetBootROMOffset(u32 address) const +{ + return address & (BOOT_ROM_SIZE - 1); +} + +u32 StarletMemory::GetSRAMOffset(u32 address) const +{ + const u32 window_offset = address & (SRAM_WINDOW_SIZE - 1); + if (!m_sram_split_mode) + { + if (window_offset < SRAM_A_SIZE) + return window_offset; + if (window_offset < SRAM_A_SIZE + SRAM_B_SIZE) + return SRAM_A_SIZE + window_offset - SRAM_A_SIZE; + return INVALID_SRAM_OFFSET; + } + + if (window_offset < SRAM_B_SIZE) + return SRAM_A_SIZE + window_offset; + if (window_offset >= SRAM_A_SIZE) + return window_offset - SRAM_A_SIZE; + return INVALID_SRAM_OFFSET; +} + +u8 StarletMemory::ReadMapped8(u32 address) const +{ + if (IsMemoryAddress(address)) + return m_system.GetMemory().Read_U8(address); + if (IsSRAMWindowAddress(address)) + { + const u32 offset = GetSRAMOffset(address); + return offset == INVALID_SRAM_OFFSET ? 0 : m_sram[offset]; + } + const auto it = m_registers.find(address); + return it == m_registers.end() ? 0 : it->second; +} + +void StarletMemory::WriteMapped8(u32 address, u8 value) +{ + if (IsMemoryAddress(address)) + { + m_system.GetMemory().Write_U8(value, address); + return; + } + if (IsSRAMWindowAddress(address)) + { + const u32 offset = GetSRAMOffset(address); + if (offset != INVALID_SRAM_OFFSET) + m_sram[offset] = value; + return; + } + m_registers[address] = value; +} + +u32 StarletMemory::ReadRegister(u32 address) const +{ + const u32 b0 = ReadMapped8(address); + const u32 b1 = ReadMapped8(address + 1); + const u32 b2 = ReadMapped8(address + 2); + const u32 b3 = ReadMapped8(address + 3); + return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3; +} + +bool StarletMemory::IsEHCIAddress(u32 address) +{ + return address >= EHCI_BASE && address < EHCI_BASE + EHCI_SIZE; +} + +u32 StarletMemory::ReadEHCIRegister(u32 address) const +{ + const u32 offset = address - EHCI_BASE; + switch (offset) + { + case EHCI_CAPLENVER: + // EHCI 1.0, with operational registers starting at offset 0x10. + return 0x01000010; + case EHCI_HCSPARAMS: + // Two external ports, routed to one companion controller with two ports. + return 0x00001202; + case EHCI_HCCPARAMS: + // Programmable frame-list size and asynchronous schedule park capability. + return 0x00000006; + case EHCI_PORT_ROUTE: + return 0; + case EHCI_USB_INTERRUPT: + return m_ehci_interrupt_enable; + default: + return ReadRegister(address); + } +} + +void StarletMemory::ResetEHCIController(bool preserve_phy_registers) +{ + const u32 first_offset = preserve_phy_registers ? EHCI_USB_COMMAND : 0; + const u32 end_offset = preserve_phy_registers ? EHCI_OPERATIONAL_RESET_END : EHCI_SIZE; + for (u32 offset = first_offset; offset < end_offset; offset += 4) + WriteRegister(EHCI_BASE + offset, 0); + + m_ehci_interrupt_enable = 0; + m_ehci_status_before_write = 0; + m_ehci_microframe_cycle_remainder = 0; + m_ehci_running = false; + WriteRegister(EHCI_BASE + EHCI_USB_COMMAND, EHCI_COMMAND_HOLLYWOOD_RESET_VALUE); + WriteRegister(EHCI_BASE + EHCI_USB_STATUS, EHCI_STATUS_HALTED); + UpdateEHCIInterrupt(); +} + +void StarletMemory::SetEHCIInterruptStatus(u32 status) +{ + WriteRegister(EHCI_BASE + EHCI_USB_STATUS, + ReadRegister(EHCI_BASE + EHCI_USB_STATUS) | (status & EHCI_INTERRUPT_MASK)); + UpdateEHCIInterrupt(); +} + +void StarletMemory::UpdateEHCIInterrupt() +{ + const u32 pending = + ReadRegister(EHCI_BASE + EHCI_USB_STATUS) & m_ehci_interrupt_enable & EHCI_INTERRUPT_MASK; + m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_EHCI, pending != 0); +} + +void StarletMemory::HandleEHCIWrite(u32 address) +{ + const u32 offset = address - EHCI_BASE; + const u32 value = ReadRegister(address); + const Starlet* const starlet = m_system.GetStarlet(); + DEBUG_LOG_FMT(IOS_USB, "Starlet EHCI write register {:#04x} = {:#010x} at PC {:#010x}", offset, + value, starlet ? starlet->GetPC() : 0); + + switch (offset) + { + case EHCI_USB_COMMAND: + { + if ((value & EHCI_COMMAND_RESET) != 0) + { + // HCRESET resets the EHCI operational state but does not power-cycle the + // Hollywood USB PHY. IOS configures vendor registers at 0xa4-0xcc before + // issuing this reset and relies on that setup remaining intact. + ResetEHCIController(true); + break; + } + + m_ehci_running = (value & EHCI_COMMAND_RUN) != 0; + WriteRegister(address, value & ~EHCI_COMMAND_RESET); + u32 status = ReadRegister(EHCI_BASE + EHCI_USB_STATUS); + if (m_ehci_running) + status &= ~EHCI_STATUS_HALTED; + else + status |= EHCI_STATUS_HALTED; + if (m_ehci_running && (value & EHCI_COMMAND_PERIODIC_ENABLE) != 0) + status |= EHCI_STATUS_PERIODIC_ACTIVE; + else + status &= ~EHCI_STATUS_PERIODIC_ACTIVE; + if (m_ehci_running && (value & EHCI_COMMAND_ASYNC_ENABLE) != 0) + status |= EHCI_STATUS_ASYNC_ACTIVE; + else + status &= ~EHCI_STATUS_ASYNC_ACTIVE; + WriteRegister(EHCI_BASE + EHCI_USB_STATUS, status); + if ((value & EHCI_COMMAND_ASYNC_ADVANCE) != 0) + SetEHCIInterruptStatus(EHCI_STATUS_ASYNC_ADVANCE); + else + UpdateEHCIInterrupt(); + break; + } + case EHCI_USB_STATUS: + WriteRegister(address, m_ehci_status_before_write & ~(value & EHCI_INTERRUPT_MASK)); + UpdateEHCIInterrupt(); + break; + case EHCI_USB_INTERRUPT: + m_ehci_interrupt_enable = value & EHCI_INTERRUPT_MASK; + WriteRegister(address, m_ehci_interrupt_enable); + UpdateEHCIInterrupt(); + break; + case EHCI_FRAME_INDEX: + WriteRegister(address, value & 0x3fff); + break; + case EHCI_PERIODIC_LIST_BASE: + WriteRegister(address, value & 0xfffff000); + break; + case EHCI_ASYNC_LIST_ADDRESS: + WriteRegister(address, value & 0xffffffe0); + break; + case EHCI_PORT_STATUS_1: + case EHCI_PORT_STATUS_2: + { + // No high-speed devices are currently attached. Preserve the software- + // controlled power/owner/suspend/reset bits and implement change-bit W1C. + const u32 old_port = m_ehci_status_before_write; + u32 port = old_port & ~(value & EHCI_PORT_WRITE_CLEAR_MASK); + port &= ~(EHCI_PORT_POWER | EHCI_PORT_OWNER | EHCI_PORT_SUSPEND | EHCI_PORT_RESET); + port |= value & (EHCI_PORT_POWER | EHCI_PORT_OWNER | EHCI_PORT_SUSPEND | EHCI_PORT_RESET); + port &= ~(EHCI_PORT_CONNECT_STATUS | EHCI_PORT_ENABLE); + WriteRegister(address, port); + UpdateEHCIInterrupt(); + break; + } + case EHCI_CAPLENVER: + case EHCI_HCSPARAMS: + case EHCI_HCCPARAMS: + case EHCI_PORT_ROUTE: + break; + default: + break; + } +} + +void StarletMemory::AdvanceEHCI(u64 cycles) +{ + if (!m_ehci_running) + return; + + m_ehci_microframe_cycle_remainder += cycles; + while (m_ehci_microframe_cycle_remainder >= EHCI_MICROFRAME_CYCLES) + { + m_ehci_microframe_cycle_remainder -= EHCI_MICROFRAME_CYCLES; + const u32 old_index = ReadRegister(EHCI_BASE + EHCI_FRAME_INDEX) & 0x3fff; + const u32 index = (old_index + 1) & 0x3fff; + WriteRegister(EHCI_BASE + EHCI_FRAME_INDEX, index); + if (index == 0) + SetEHCIInterruptStatus(EHCI_STATUS_FRAME_ROLLOVER); + } +} + +std::optional StarletMemory::GetOHCIControllerIndex(u32 address) +{ + for (size_t controller = 0; controller < OHCI_BASES.size(); ++controller) + { + if (address >= OHCI_BASES[controller] && address < OHCI_BASES[controller] + OHCI_SIZE) + return controller; + } + return std::nullopt; +} + +u32 StarletMemory::ReadOHCIRegister(size_t controller, u32 address) const +{ + const u32 base = OHCI_BASES[controller]; + const u32 offset = address - base; + switch (offset) + { + case OHCI_REVISION: + return 0x10; // OpenHCI 1.0. + case OHCI_INTERRUPT_ENABLE: + case OHCI_INTERRUPT_DISABLE: + return m_ohci_interrupt_enable[controller]; + case OHCI_INTERRUPT_STATUS: + case OHCI_RH_DESCRIPTOR_A: + case OHCI_RH_DESCRIPTOR_B: + case OHCI_RH_STATUS: + case OHCI_RH_PORT_STATUS_1: + case OHCI_RH_PORT_STATUS_2: + return ReadRegister(address); + case OHCI_FRAME_REMAINING: + { + if (!m_ohci_operational[controller]) + return 0; + const u32 frame_interval = ReadRegister(base + OHCI_FRAME_INTERVAL); + const u32 interval = frame_interval & 0x3fff; + const u64 elapsed = m_ohci_frame_cycle_remainder[controller]; + const u32 remaining = interval - static_cast(elapsed * (interval + 1) / OHCI_FRAME_CYCLES); + return (frame_interval & 0x80000000) | (remaining & 0x3fff); + } + default: + return ReadRegister(address); + } +} + +void StarletMemory::ResetOHCIController(size_t controller) +{ + const u32 base = OHCI_BASES[controller]; + for (u32 offset = 0; offset < OHCI_SIZE; offset += 4) + WriteRegister(base + offset, 0); + + m_ohci_interrupt_enable[controller] = 0; + m_ohci_interrupt_status_before_write[controller] = 0; + m_ohci_port_status_before_write[controller] = {}; + m_ohci_port_reset_frames[controller] = {}; + m_ohci_frame_cycle_remainder[controller] = 0; + m_ohci_operational[controller] = false; + if (controller == 1) + { + m_ohci1_attach_delay_frames = 0; + m_ohci1_hci_events.clear(); + m_ohci1_acl_packets.clear(); + m_ohci1_acl_delay_frames = 0; + m_ohci1_hci_command_pending = false; + } + WriteRegister(base + OHCI_FRAME_INTERVAL, OHCI_DEFAULT_FRAME_INTERVAL); + WriteRegister(base + OHCI_LOW_SPEED_THRESHOLD, OHCI_DEFAULT_LOW_SPEED_THRESHOLD); + + // OHCI0 owns the two external full-speed companion ports. OHCI1 is the + // internal bus and has one permanently wired full-speed BCM2045 Bluetooth + // module. Its USB pull-up becomes visible shortly after IOS powers the port; + // the delay keeps the connection-change event asynchronous, as it is on the + // physical bus, instead of interrupting the driver in the middle of its setup + // transaction. + const u32 port_count = controller == 0 ? 2 : 1; + WriteRegister(base + OHCI_RH_DESCRIPTOR_A, + OHCI_POWER_ON_TO_POWER_GOOD_TIME | (1U << 9) | (1U << 12) | port_count); + // DeviceRemovable uses bit 1 for downstream port 1 (bit 0 is reserved). The + // Bluetooth daughter board is permanently wired to OHCI1; the two OHCI0 + // companion ports remain removable. + WriteRegister(base + OHCI_RH_DESCRIPTOR_B, controller == 1 ? 1U << 1 : 0); + if (controller == 1) + { + WriteRegister(base + OHCI_RH_PORT_STATUS_1, OHCI_PORT_CURRENT_CONNECT_STATUS | + OHCI_PORT_POWER_STATUS | + OHCI_PORT_CONNECT_STATUS_CHANGE); + } + UpdateOHCIInterrupt(controller); +} + +void StarletMemory::SetOHCIInterruptStatus(size_t controller, u32 status) +{ + const u32 address = OHCI_BASES[controller] + OHCI_INTERRUPT_STATUS; + WriteRegister(address, ReadRegister(address) | status); + UpdateOHCIInterrupt(controller); +} + +void StarletMemory::UpdateOHCIInterrupt(size_t controller) +{ + const u32 pending = ReadRegister(OHCI_BASES[controller] + OHCI_INTERRUPT_STATUS) & + m_ohci_interrupt_enable[controller] & ~OHCI_INTERRUPT_MASTER_ENABLE; + const bool master_enabled = + (m_ohci_interrupt_enable[controller] & OHCI_INTERRUPT_MASTER_ENABLE) != 0; + const u32 cause = controller == 0 ? INT_CAUSE_OHCI0 : INT_CAUSE_OHCI1; + m_system.GetWiiIPC().SetStarletInterrupt(cause, master_enabled && pending != 0); +} + +void StarletMemory::HandleOHCIWrite(size_t controller, u32 address) +{ + const u32 base = OHCI_BASES[controller]; + const u32 offset = address - base; + const u32 value = ReadRegister(address); + const Starlet* starlet = m_system.GetStarlet(); + DEBUG_LOG_FMT(IOS_USB, "Starlet OHCI{} write register {:#04x} = {:#010x} at PC {:#010x}", + controller, offset, value, starlet ? starlet->GetPC() : 0); + switch (offset) + { + case OHCI_CONTROL: + m_ohci_operational[controller] = + (value & OHCI_CONTROL_FUNCTIONAL_STATE_MASK) == OHCI_CONTROL_FUNCTIONAL_STATE_OPERATIONAL; + if (m_ohci_operational[controller] && + ((ReadRegister(base + OHCI_RH_PORT_STATUS_1) | ReadRegister(base + OHCI_RH_PORT_STATUS_2)) & + OHCI_PORT_CHANGE_MASK) != 0) + { + SetOHCIInterruptStatus(controller, OHCI_INTERRUPT_ROOT_HUB_STATUS_CHANGE); + } + if (m_ohci_operational[controller]) + ProcessOHCISchedules(controller); + break; + case OHCI_COMMAND_STATUS: + if ((value & OHCI_COMMAND_HOST_CONTROLLER_RESET) != 0) + { + ResetOHCIController(controller); + break; + } + // CLF and BLF remain set until the corresponding schedule has been + // consumed. IOS can therefore observe that the hardware accepted the + // doorbell before ED/TD processing completes. + WriteRegister(address, + value & (OHCI_COMMAND_CONTROL_LIST_FILLED | OHCI_COMMAND_BULK_LIST_FILLED)); + ProcessOHCISchedules(controller); + break; + case OHCI_INTERRUPT_STATUS: + WriteRegister(address, m_ohci_interrupt_status_before_write[controller] & ~value); + UpdateOHCIInterrupt(controller); + break; + case OHCI_INTERRUPT_ENABLE: + m_ohci_interrupt_enable[controller] |= value; + WriteRegister(address, m_ohci_interrupt_enable[controller]); + UpdateOHCIInterrupt(controller); + break; + case OHCI_INTERRUPT_DISABLE: + m_ohci_interrupt_enable[controller] &= ~value; + WriteRegister(base + OHCI_INTERRUPT_ENABLE, m_ohci_interrupt_enable[controller]); + WriteRegister(address, m_ohci_interrupt_enable[controller]); + UpdateOHCIInterrupt(controller); + break; + case OHCI_HCCA: + WriteRegister(address, value & 0xffffff00); + break; + case OHCI_CONTROL_HEAD_ED: + case OHCI_CONTROL_CURRENT_ED: + case OHCI_BULK_HEAD_ED: + case OHCI_BULK_CURRENT_ED: + case OHCI_PERIOD_CURRENT_ED: + case OHCI_DONE_HEAD: + WriteRegister(address, value & 0xfffffff0); + break; + case OHCI_RH_DESCRIPTOR_A: + WriteRegister(address, (value & ~0xffU) | (controller == 0 ? 2U : 1U)); + break; + case OHCI_RH_STATUS: + { + // Local/global power switching is immediate. OHCI1's internal module + // remains physically connected even while bus power is disabled. + for (u32 port_offset : {OHCI_RH_PORT_STATUS_1, OHCI_RH_PORT_STATUS_2}) + { + u32 port = ReadRegister(base + port_offset); + if ((value & (1U << 0)) != 0) + port &= ~OHCI_PORT_POWER_STATUS; + if ((value & (1U << 16)) != 0) + port |= OHCI_PORT_POWER_STATUS; + WriteRegister(base + port_offset, port); + } + WriteRegister(address, 0); + break; + } + case OHCI_RH_PORT_STATUS_1: + case OHCI_RH_PORT_STATUS_2: + { + const size_t port_index = (offset - OHCI_RH_PORT_STATUS_1) / 4; + u32 port = m_ohci_port_status_before_write[controller][port_index]; + port &= ~(value & OHCI_PORT_CHANGE_MASK); + if ((value & (1U << 0)) != 0) + port &= ~OHCI_PORT_ENABLE_STATUS; + if ((value & (1U << 1)) != 0 && (port & OHCI_PORT_CURRENT_CONNECT_STATUS) != 0) + port |= OHCI_PORT_ENABLE_STATUS; + if ((value & (1U << 2)) != 0 && (port & OHCI_PORT_ENABLE_STATUS) != 0) + port |= OHCI_PORT_SUSPEND_STATUS; + if ((value & (1U << 3)) != 0) + { + port &= ~OHCI_PORT_SUSPEND_STATUS; + port |= OHCI_PORT_SUSPEND_STATUS_CHANGE; + } + if ((value & (1U << 4)) != 0 && (port & OHCI_PORT_CURRENT_CONNECT_STATUS) != 0) + { + // OpenHCI asserts reset for 10 ms. Completing it synchronously can make + // the IOS hub state machine observe PRSC before it has armed the waiter + // for that event. + port |= OHCI_PORT_RESET_STATUS; + port &= ~(OHCI_PORT_ENABLE_STATUS | OHCI_PORT_SUSPEND_STATUS); + m_ohci_port_reset_frames[controller][port_index] = OHCI_PORT_RESET_FRAMES; + if (controller == 1 && port_index == 0) + { + m_ohci1_device_address = 0; + m_ohci1_pending_address = 0xff; + m_ohci1_configuration = 0; + m_ohci1_control_request_valid = false; + m_ohci1_control_stalled = false; + m_ohci1_control_response.clear(); + m_ohci1_hci_events.clear(); + m_ohci1_hci_command_pending = false; + } + } + if ((value & (1U << 8)) != 0) + { + port |= OHCI_PORT_POWER_STATUS; + if (controller == 1 && port_index == 0 && (port & OHCI_PORT_CURRENT_CONNECT_STATUS) == 0) + m_ohci1_attach_delay_frames = OHCI1_ATTACH_DELAY_FRAMES; + } + if ((value & (1U << 9)) != 0) + { + port &= ~(OHCI_PORT_CURRENT_CONNECT_STATUS | OHCI_PORT_ENABLE_STATUS | + OHCI_PORT_SUSPEND_STATUS | OHCI_PORT_RESET_STATUS | OHCI_PORT_POWER_STATUS); + m_ohci_port_reset_frames[controller][port_index] = 0; + if (controller == 1 && port_index == 0) + m_ohci1_attach_delay_frames = 0; + } + WriteRegister(address, port); + if ((port & OHCI_PORT_CHANGE_MASK) != 0) + SetOHCIInterruptStatus(controller, OHCI_INTERRUPT_ROOT_HUB_STATUS_CHANGE); + break; + } + case OHCI_REVISION: + case OHCI_FRAME_REMAINING: + case OHCI_FRAME_NUMBER: + break; + default: + break; + } +} + +void StarletMemory::AdvanceOHCI(size_t controller, u64 cycles) +{ + const u32 base = OHCI_BASES[controller]; + if (!m_ohci_operational[controller]) + return; + + m_ohci_frame_cycle_remainder[controller] += cycles; + while (m_ohci_frame_cycle_remainder[controller] >= OHCI_FRAME_CYCLES) + { + m_ohci_frame_cycle_remainder[controller] -= OHCI_FRAME_CYCLES; + const u16 old_frame = static_cast(ReadRegister(base + OHCI_FRAME_NUMBER)); + const u16 frame = static_cast(old_frame + 1); + WriteRegister(base + OHCI_FRAME_NUMBER, frame); + + for (size_t port_index = 0; port_index < m_ohci_port_reset_frames[controller].size(); + ++port_index) + { + u8& reset_frames = m_ohci_port_reset_frames[controller][port_index]; + if (reset_frames == 0 || --reset_frames != 0) + continue; + + const u32 port_address = base + OHCI_RH_PORT_STATUS_1 + static_cast(port_index) * 4; + u32 port = ReadRegister(port_address); + port &= + ~(OHCI_PORT_RESET_STATUS | OHCI_PORT_SUSPEND_STATUS | OHCI_PORT_SUSPEND_STATUS_CHANGE); + if ((port & OHCI_PORT_CURRENT_CONNECT_STATUS) != 0) + port |= OHCI_PORT_ENABLE_STATUS | OHCI_PORT_RESET_STATUS_CHANGE; + WriteRegister(port_address, port); + SetOHCIInterruptStatus(controller, OHCI_INTERRUPT_ROOT_HUB_STATUS_CHANGE); + } + + if (controller == 1 && m_ohci1_attach_delay_frames != 0 && --m_ohci1_attach_delay_frames == 0) + { + const u32 port_address = base + OHCI_RH_PORT_STATUS_1; + u32 port = ReadRegister(port_address); + if ((port & OHCI_PORT_POWER_STATUS) != 0) + { + port |= OHCI_PORT_CURRENT_CONNECT_STATUS | OHCI_PORT_CONNECT_STATUS_CHANGE; + WriteRegister(port_address, port); + SetOHCIInterruptStatus(controller, OHCI_INTERRUPT_ROOT_HUB_STATUS_CHANGE); + } + } + + if (controller == 1 && m_ohci1_acl_delay_frames != 0) + --m_ohci1_acl_delay_frames; + + // HCCA is an OpenHCI DMA structure and therefore little-endian even though + // Starlet's MMIO register view is reversed for its big-endian CPU. + const u32 hcca = ReadRegister(base + OHCI_HCCA); + if (hcca != 0) + { + WriteMapped8(hcca + 0x80, static_cast(frame)); + WriteMapped8(hcca + 0x81, static_cast(frame >> 8)); + } + ProcessOHCISchedules(controller); + SetOHCIInterruptStatus(controller, OHCI_INTERRUPT_START_OF_FRAME); + if ((old_frame & 0x8000) != (frame & 0x8000)) + SetOHCIInterruptStatus(controller, OHCI_INTERRUPT_FRAME_NUMBER_OVERFLOW); + } +} + +u32 StarletMemory::ReadOHCIMemory32(u32 address) const +{ + return static_cast(ReadMapped8(address)) | static_cast(ReadMapped8(address + 1)) << 8 | + static_cast(ReadMapped8(address + 2)) << 16 | + static_cast(ReadMapped8(address + 3)) << 24; +} + +void StarletMemory::WriteOHCIMemory32(u32 address, u32 value) +{ + WriteMapped8(address, static_cast(value)); + WriteMapped8(address + 1, static_cast(value >> 8)); + WriteMapped8(address + 2, static_cast(value >> 16)); + WriteMapped8(address + 3, static_cast(value >> 24)); +} + +std::vector StarletMemory::ReadOHCIBuffer(u32 current_buffer, u32 buffer_end) const +{ + if (current_buffer == 0) + return {}; + + const bool same_page = (current_buffer & ~0xfffU) == (buffer_end & ~0xfffU); + if (same_page && buffer_end < current_buffer) + return {}; + const size_t first_size = + same_page ? buffer_end - current_buffer + 1 : 0x1000 - (current_buffer & 0xfff); + const size_t second_size = same_page ? 0 : (buffer_end & 0xfff) + 1; + if (first_size + second_size > 0x2000) + return {}; + + std::vector buffer(first_size + second_size); + for (size_t i = 0; i < first_size; ++i) + buffer[i] = ReadMapped8(current_buffer + static_cast(i)); + const u32 second_page = buffer_end & ~0xfffU; + for (size_t i = 0; i < second_size; ++i) + buffer[first_size + i] = ReadMapped8(second_page + static_cast(i)); + return buffer; +} + +void StarletMemory::WriteOHCIBuffer(u32 current_buffer, u32 buffer_end, const u8* data, size_t size) +{ + if (current_buffer == 0 || size == 0) + return; + + const bool same_page = (current_buffer & ~0xfffU) == (buffer_end & ~0xfffU); + const size_t first_capacity = + same_page ? buffer_end - current_buffer + 1 : 0x1000 - (current_buffer & 0xfff); + const size_t first_size = std::min(size, first_capacity); + for (size_t i = 0; i < first_size; ++i) + WriteMapped8(current_buffer + static_cast(i), data[i]); + if (size > first_size) + { + const u32 second_page = buffer_end & ~0xfffU; + const size_t second_size = + std::min(size - first_size, static_cast((buffer_end & 0xfff) + 1)); + for (size_t i = 0; i < second_size; ++i) + WriteMapped8(second_page + static_cast(i), data[first_size + i]); + } +} + +std::vector StarletMemory::GetOHCI1Descriptor(u16 value) const +{ + const u8 descriptor_type = static_cast(value >> 8); + const u8 descriptor_index = static_cast(value); + if (descriptor_type == 1 && descriptor_index == 0) + { + // Nintendo's internal BCM2045 presents the standard wireless-controller USB + // class tuple and the VID/PID used by IOS to create /dev/usb/oh1/57e/305. + return {0x12, 0x01, 0x10, 0x02, 0xe0, 0x01, 0x01, 0x40, 0x7e, + 0x05, 0x05, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01}; + } + if (descriptor_type == 2 && descriptor_index == 0) + { + return { + 0x09, 0x02, 0x27, 0x00, 0x01, 0x01, 0x00, 0xe0, 0x00, // Configuration. + 0x09, 0x04, 0x00, 0x00, 0x03, 0xe0, 0x01, 0x01, 0x00, // Bluetooth interface. + 0x07, 0x05, 0x81, 0x03, 0x10, 0x00, 0x01, // HCI event interrupt IN. + 0x07, 0x05, 0x82, 0x02, 0x40, 0x00, 0x00, // ACL bulk IN. + 0x07, 0x05, 0x02, 0x02, 0x40, 0x00, 0x00, // ACL bulk OUT. + }; + } + if (descriptor_type == 3 && descriptor_index == 0) + return {0x04, 0x03, 0x09, 0x04}; + return {}; +} + +void StarletMemory::QueueOHCI1HCIEvent(u8 event_code, const void* parameters, size_t parameter_size) +{ + if (parameter_size > 0xff) + return; + + std::vector event; + event.reserve(2 + parameter_size); + event.push_back(event_code); + event.push_back(static_cast(parameter_size)); + if (parameter_size != 0) + { + const auto* bytes = static_cast(parameters); + event.insert(event.end(), bytes, bytes + parameter_size); + } + m_ohci1_hci_events.emplace_back(std::move(event)); +} + +void StarletMemory::QueueOHCI1HCICommandComplete(u16 opcode, + const std::vector& return_parameters) +{ + std::vector parameters = {1, static_cast(opcode), static_cast(opcode >> 8)}; + parameters.insert(parameters.end(), return_parameters.begin(), return_parameters.end()); + QueueOHCI1HCIEvent(HCI_EVENT_COMMAND_COMPL, parameters.data(), parameters.size()); +} + +void StarletMemory::QueueOHCI1HCICommandStatus(u16 opcode, u8 status) +{ + const std::array parameters = {status, 1, static_cast(opcode), + static_cast(opcode >> 8)}; + QueueOHCI1HCIEvent(HCI_EVENT_COMMAND_STATUS, parameters.data(), parameters.size()); +} + +u16 StarletMemory::GetWiimoteConnectionHandle(const bdaddr_t& address) const +{ + for (size_t index = 0; index < m_wiimotes.size(); ++index) + { + if (m_wiimotes[index] && m_wiimotes[index]->GetBD() == address) + return static_cast(0x100 + index); + } + return 0; +} + +IOS::HLE::WiimoteDevice* StarletMemory::AccessWiimote(const bdaddr_t& address) +{ + const auto it = std::ranges::find_if(m_wiimotes, [&address](const auto& wiimote) { + return wiimote && wiimote->GetBD() == address; + }); + return it != m_wiimotes.end() ? it->get() : nullptr; +} + +IOS::HLE::WiimoteDevice* StarletMemory::AccessWiimote(u16 connection_handle) +{ + if (connection_handle < 0x100) + return nullptr; + const size_t index = connection_handle - 0x100; + return index < m_wiimotes.size() ? m_wiimotes[index].get() : nullptr; +} + +void StarletMemory::QueueOHCI1ConnectionComplete(const bdaddr_t& address, u8 status) +{ + std::vector parameters = {status, static_cast(GetWiimoteConnectionHandle(address)), + static_cast(GetWiimoteConnectionHandle(address) >> 8)}; + parameters.insert(parameters.end(), address.begin(), address.end()); + parameters.push_back(HCI_LINK_ACL); + // Authentication is completed separately in response to HCI_CMD_AUTH_REQ. + parameters.push_back(HCI_ENCRYPTION_MODE_NONE); + QueueOHCI1HCIEvent(HCI_EVENT_CON_COMPL, parameters.data(), parameters.size()); +} + +void StarletMemory::QueueOHCI1CompletedPackets(u16 connection_handle, u16 packet_count) +{ + const std::array parameters = { + 1, static_cast(connection_handle), static_cast(connection_handle >> 8), + static_cast(packet_count), static_cast(packet_count >> 8)}; + QueueOHCI1HCIEvent(HCI_EVENT_NUM_COMPL_PKTS, parameters.data(), parameters.size()); +} + +void StarletMemory::InitializeWiimotes() +{ + for (size_t i = 0; i < m_wiimotes.size(); ++i) + { + if (std::ranges::all_of(m_wiimote_addresses[i], [](u8 byte) { return byte == 0; })) + m_wiimote_addresses[i] = {0x11, 0x02, 0x19, 0x79, 0, static_cast(i)}; + if (m_wiimotes[i]) + m_wiimotes[i]->SetSource(nullptr); + m_wiimotes[i] = std::make_unique( + this, m_wiimote_addresses[i], static_cast(i), static_cast(i)); + // The LLE boot replaces the placeholder devices after Wiimote::Initialize + // has already propagated the configured sources. Rebind the final devices + // so input can reconnect through the original IOS Bluetooth stack. + m_wiimotes[i]->SetSource(WiimoteCommon::GetHIDWiimoteSource(static_cast(i))); + DEBUG_LOG_FMT(IOS_WIIMOTE, "Starlet Wii Remote {} source valid: {}", i, + m_wiimotes[i]->IsSourceValid()); + } +} + +void StarletMemory::SetWiimoteSource(size_t index, WiimoteCommon::HIDWiimote* source) +{ + if (index < m_wiimotes.size() && m_wiimotes[index]) + m_wiimotes[index]->SetSource(source); +} + +void StarletMemory::UpdateWiimotes() +{ + for (auto& wiimote : m_wiimotes) + { + if (wiimote) + wiimote->Update(); + } + + g_controller_interface.SetCurrentInputChannel(ciface::InputChannel::Bluetooth); + g_controller_interface.UpdateInput(); + + std::array states{}; + std::array next_calls{}; + for (size_t i = 0; i < m_wiimotes.size(); ++i) + { + if (m_wiimotes[i]) + next_calls[i] = m_wiimotes[i]->PrepareInput(&states[i]); + } + + const u64 previous_update_cycles = + m_arm_cycles >= WIIMOTE_UPDATE_CYCLES ? m_arm_cycles - WIIMOTE_UPDATE_CYCLES : 0; + const bool report_due = + m_arm_cycles / WIIMOTE_REPORT_CYCLES != previous_update_cycles / WIIMOTE_REPORT_CYCLES; + + for (size_t i = 0; i < m_wiimotes.size(); ++i) + { + if (!m_wiimotes[i]) + continue; + + const bool button_changed = states[i].buttons.hex != m_last_wiimote_buttons[i]; + if (next_calls[i] != IOS::HLE::WiimoteDevice::NextUpdateInputCall::Update || report_due || + button_changed) + { + m_wiimotes[i]->UpdateInput(next_calls[i], states[i]); + if (next_calls[i] == IOS::HLE::WiimoteDevice::NextUpdateInputCall::Update) + m_last_wiimote_buttons[i] = states[i].buttons.hex; + } + } +} + +void StarletMemory::SendACLPacket(const bdaddr_t& source, const u8* data, u32 size) +{ + if (size > 0xffff || m_ohci1_acl_packets.size() >= 100) + return; + + const u16 connection_handle = GetWiimoteConnectionHandle(source); + const u16 handle_and_flags = + HCI_MK_CON_HANDLE(connection_handle, HCI_PACKET_START, HCI_POINT2POINT); + std::vector packet = {static_cast(handle_and_flags), + static_cast(handle_and_flags >> 8), static_cast(size), + static_cast(size >> 8)}; + packet.insert(packet.end(), data, data + size); + m_ohci1_acl_packets.emplace_back(std::move(packet)); + DEBUG_LOG_FMT(IOS_WIIMOTE, "Starlet queued ACL IN for handle {:#06x}: {}", connection_handle, + HexDump(data, size)); +} + +bool StarletMemory::RemoteConnect(IOS::HLE::WiimoteDevice& wiimote) +{ + if ((m_ohci1_scan_enable & HCI_PAGE_SCAN_ENABLE) == 0) + return false; + + std::vector parameters(wiimote.GetBD().begin(), wiimote.GetBD().end()); + parameters.insert(parameters.end(), wiimote.GetClass().begin(), wiimote.GetClass().end()); + parameters.push_back(HCI_LINK_ACL); + QueueOHCI1HCIEvent(HCI_EVENT_CON_REQ, parameters.data(), parameters.size()); + INFO_LOG_FMT(IOS_WIIMOTE, "Starlet queued Wii Remote {} HCI connection request", + wiimote.GetNumber()); + return true; +} + +bool StarletMemory::RemoteDisconnect(const bdaddr_t& address) +{ + const u16 connection_handle = GetWiimoteConnectionHandle(address); + const std::array parameters = {0, static_cast(connection_handle), + static_cast(connection_handle >> 8), 0x13}; + QueueOHCI1HCIEvent(HCI_EVENT_DISCON_COMPL, parameters.data(), parameters.size()); + return true; +} + +bool StarletMemory::IsSensorBarEnabled() const +{ + return m_system.GetWiiIPC().GetGPIOOutFlags()[IOS::GPIO::SENSOR_BAR] ? true : false; +} + +void StarletMemory::ExecuteOHCI1ACLPacket(std::vector* packet) +{ + if (packet->size() < sizeof(hci_acldata_hdr_t)) + return; + + const u16 handle_and_flags = static_cast((*packet)[0] | ((*packet)[1] << 8)); + const u16 payload_size = static_cast((*packet)[2] | ((*packet)[3] << 8)); + if (payload_size > packet->size() - sizeof(hci_acldata_hdr_t)) + return; + + const u16 connection_handle = HCI_CON_HANDLE(handle_and_flags); + DEBUG_LOG_FMT(IOS_WIIMOTE, "Starlet received ACL OUT for handle {:#06x}: {}", connection_handle, + HexDump(packet->data() + sizeof(hci_acldata_hdr_t), payload_size)); + if (IOS::HLE::WiimoteDevice* wiimote = AccessWiimote(connection_handle)) + { + wiimote->ExecuteL2capCmd(packet->data() + sizeof(hci_acldata_hdr_t), payload_size); + QueueOHCI1CompletedPackets(connection_handle, 1); + } +} + +void StarletMemory::ExecuteOHCI1HCICommand(const std::vector& command) +{ + if (command.size() < 3) + return; + + const u16 opcode = static_cast(command[0] | (static_cast(command[1]) << 8)); + const size_t parameter_size = command[2]; + if (command.size() < 3 + parameter_size) + return; + + DEBUG_LOG_FMT(IOS_USB, "Starlet OHCI1 HCI command {:#06x}, parameter length {}", opcode, + parameter_size); + + const auto append_le16 = [](std::vector* output, u16 value) { + output->push_back(static_cast(value)); + output->push_back(static_cast(value >> 8)); + }; + + std::vector reply = {0}; // HCI success status. + switch (opcode) + { + case HCI_CMD_RESET: + m_ohci1_hci_events.clear(); + m_ohci1_acl_packets.clear(); + m_ohci1_acl_delay_frames = 0; + m_ohci1_scan_enable = 0; + for (auto& wiimote : m_wiimotes) + { + if (wiimote) + wiimote->EventControllerReset(); + } + break; + case HCI_CMD_READ_LOCAL_VER: + reply.push_back(0x03); + append_le16(&reply, 0x40a7); + reply.push_back(0x03); + append_le16(&reply, 0x000f); + append_le16(&reply, 0x430e); + break; + case HCI_CMD_READ_LOCAL_FEATURES: + reply.insert(reply.end(), {0xff, 0xff, 0x8d, 0xfe, 0x9b, 0xf9, 0x00, 0x80}); + break; + case HCI_CMD_READ_BUFFER_SIZE: + append_le16(&reply, 339); + reply.push_back(64); + append_le16(&reply, 10); + append_le16(&reply, 0); + break; + case HCI_CMD_READ_BDADDR: + reply.insert(reply.end(), {0x11, 0x02, 0x19, 0x79, 0x00, 0xff}); + break; + case HCI_CMD_READ_STORED_LINK_KEY: + { + const bool read_all = parameter_size >= sizeof(hci_read_stored_link_key_cp) && command[9] != 0; + const u16 key_count = read_all ? static_cast(m_wiimotes.size()) : 0; + append_le16(&reply, 255); + append_le16(&reply, key_count); + if (key_count != 0) + { + std::vector parameters = {static_cast(key_count)}; + for (const auto& wiimote : m_wiimotes) + { + parameters.insert(parameters.end(), wiimote->GetBD().begin(), wiimote->GetBD().end()); + parameters.insert(parameters.end(), wiimote->GetLinkKey().begin(), + wiimote->GetLinkKey().end()); + } + QueueOHCI1HCIEvent(HCI_EVENT_RETURN_LINK_KEYS, parameters.data(), parameters.size()); + } + break; + } + case HCI_CMD_DELETE_STORED_LINK_KEY: + append_le16(&reply, 0); + break; + case HCI_CMD_WRITE_STORED_LINK_KEY: + reply.push_back(parameter_size == 0 ? 0 : command[3]); + break; + case HCI_CMD_INQUIRY: + { + QueueOHCI1HCICommandStatus(opcode); + const auto it = std::ranges::find_if( + m_wiimotes, [](const auto& wiimote) { return wiimote && wiimote->IsInquiryScanEnabled(); }); + if (it != m_wiimotes.end()) + { + std::vector parameters = {1}; + parameters.insert(parameters.end(), (*it)->GetBD().begin(), (*it)->GetBD().end()); + parameters.insert(parameters.end(), {1, 0, 0}); + parameters.insert(parameters.end(), (*it)->GetClass().begin(), (*it)->GetClass().end()); + parameters.insert(parameters.end(), {0x18, 0x38}); + QueueOHCI1HCIEvent(HCI_EVENT_INQUIRY_RESULT, parameters.data(), parameters.size()); + } + const std::array complete = {0}; + QueueOHCI1HCIEvent(HCI_EVENT_INQUIRY_COMPL, complete.data(), complete.size()); + return; + } + case HCI_CMD_CREATE_CON: + { + QueueOHCI1HCICommandStatus(opcode); + if (parameter_size < 6) + return; + bdaddr_t address{}; + std::copy_n(command.begin() + 3, address.size(), address.begin()); + IOS::HLE::WiimoteDevice* wiimote = AccessWiimote(address); + const bool connected = wiimote && wiimote->EventConnectionRequest(); + QueueOHCI1ConnectionComplete(address, connected ? 0 : 0x08); + return; + } + case HCI_CMD_ACCEPT_CON: + { + QueueOHCI1HCICommandStatus(opcode); + if (parameter_size < 7) + return; + bdaddr_t address{}; + std::copy_n(command.begin() + 3, address.size(), address.begin()); + IOS::HLE::WiimoteDevice* wiimote = AccessWiimote(address); + const bool connected = wiimote && wiimote->EventConnectionAccept(); + INFO_LOG_FMT(IOS_WIIMOTE, "Starlet accepted HCI connection: remote={} connected={}", + wiimote != nullptr, connected); + if (connected && command[9] == 0) + { + std::vector role_change = {0}; + role_change.insert(role_change.end(), address.begin(), address.end()); + role_change.push_back(0); + QueueOHCI1HCIEvent(HCI_EVENT_ROLE_CHANGE, role_change.data(), role_change.size()); + } + QueueOHCI1ConnectionComplete(address, connected ? 0 : 0x08); + return; + } + case HCI_CMD_DISCONNECT: + { + QueueOHCI1HCICommandStatus(opcode); + if (parameter_size < 3) + return; + const u16 handle = static_cast(command[3] | (command[4] << 8)); + const u8 reason = command[5]; + INFO_LOG_FMT(IOS_WIIMOTE, "Starlet HCI disconnect: handle={:#06x} reason={:#04x}", handle, + reason); + const std::array parameters = {0, static_cast(handle), static_cast(handle >> 8), + reason}; + QueueOHCI1HCIEvent(HCI_EVENT_DISCON_COMPL, parameters.data(), parameters.size()); + if (IOS::HLE::WiimoteDevice* wiimote = AccessWiimote(handle)) + wiimote->EventDisconnect(reason); + return; + } + case HCI_CMD_REMOTE_NAME_REQ: + { + QueueOHCI1HCICommandStatus(opcode); + if (parameter_size < 6) + return; + bdaddr_t address{}; + std::copy_n(command.begin() + 3, address.size(), address.begin()); + std::array parameters{}; + std::copy(address.begin(), address.end(), parameters.begin() + 1); + if (IOS::HLE::WiimoteDevice* wiimote = AccessWiimote(address)) + std::copy_n(wiimote->GetName(), std::min(std::strlen(wiimote->GetName()), 247), + parameters.begin() + 7); + else + parameters[0] = 0x02; + QueueOHCI1HCIEvent(HCI_EVENT_REMOTE_NAME_REQ_COMPL, parameters.data(), parameters.size()); + return; + } + case HCI_CMD_AUTH_REQ: + { + QueueOHCI1HCICommandStatus(opcode); + if (parameter_size >= 2) + { + const std::array parameters = {0, command[3], command[4]}; + QueueOHCI1HCIEvent(HCI_EVENT_AUTH_COMPL, parameters.data(), parameters.size()); + } + return; + } + case HCI_CMD_READ_REMOTE_FEATURES: + { + QueueOHCI1HCICommandStatus(opcode); + if (parameter_size >= 2) + { + const u16 handle = static_cast(command[3] | (command[4] << 8)); + std::vector parameters = {0, command[3], command[4]}; + if (IOS::HLE::WiimoteDevice* wiimote = AccessWiimote(handle)) + parameters.insert(parameters.end(), wiimote->GetFeatures().begin(), + wiimote->GetFeatures().end()); + else + parameters.insert(parameters.end(), HCI_FEATURES_SIZE, 0); + QueueOHCI1HCIEvent(HCI_EVENT_READ_REMOTE_FEATURES_COMPL, parameters.data(), + parameters.size()); + } + return; + } + case HCI_CMD_READ_REMOTE_VER_INFO: + { + QueueOHCI1HCICommandStatus(opcode); + if (parameter_size >= 2) + { + const u16 handle = static_cast(command[3] | (command[4] << 8)); + std::vector parameters = {0, command[3], command[4], 0x02, 0x0f, 0x00, 0x29, 0x02}; + if (IOS::HLE::WiimoteDevice* wiimote = AccessWiimote(handle)) + { + parameters[3] = wiimote->GetLMPVersion(); + parameters[6] = static_cast(wiimote->GetLMPSubVersion()); + parameters[7] = static_cast(wiimote->GetLMPSubVersion() >> 8); + } + QueueOHCI1HCIEvent(HCI_EVENT_READ_REMOTE_VER_INFO_COMPL, parameters.data(), + parameters.size()); + } + return; + } + case HCI_CMD_READ_CLOCK_OFFSET: + { + QueueOHCI1HCICommandStatus(opcode); + if (parameter_size >= 2) + { + const std::array parameters = {0, command[3], command[4], 0x18, 0x38}; + QueueOHCI1HCIEvent(HCI_EVENT_READ_CLOCK_OFFSET_COMPL, parameters.data(), parameters.size()); + } + return; + } + case HCI_CMD_SNIFF_MODE: + { + QueueOHCI1HCICommandStatus(opcode); + if (parameter_size >= 4) + { + const std::array parameters = {0, command[3], command[4], + 0x02, command[5], command[6]}; + QueueOHCI1HCIEvent(HCI_EVENT_MODE_CHANGE, parameters.data(), parameters.size()); + } + return; + } + case HCI_CMD_WRITE_LINK_POLICY_SETTINGS: + QueueOHCI1HCICommandStatus(opcode); + return; + case HCI_CMD_WRITE_LINK_SUPERVISION_TIMEOUT: + if (parameter_size >= 4) + { + reply.push_back(command[3]); + reply.push_back(command[4]); + } + break; + case HCI_CMD_CHANGE_CON_PACKET_TYPE: + QueueOHCI1HCICommandStatus(opcode); + if (parameter_size >= 4) + { + const std::array parameters = {0, command[3], command[4], command[5], command[6]}; + QueueOHCI1HCIEvent(HCI_EVENT_CON_PKT_TYPE_CHANGED, parameters.data(), parameters.size()); + } + return; + case HCI_CMD_SET_CON_ENCRYPTION: + QueueOHCI1HCICommandStatus(opcode); + if (parameter_size >= 3) + { + const std::array parameters = {0, command[3], command[4], command[5]}; + QueueOHCI1HCIEvent(HCI_EVENT_ENCRYPTION_CHANGE, parameters.data(), parameters.size()); + } + return; + case HCI_CMD_LINK_KEY_REP: + case HCI_CMD_LINK_KEY_NEG_REP: + case HCI_CMD_PIN_CODE_REP: + case HCI_CMD_PIN_CODE_NEG_REP: + if (parameter_size >= 6) + reply.insert(reply.end(), command.begin() + 3, command.begin() + 9); + break; + case HCI_CMD_WRITE_SCAN_ENABLE: + if (parameter_size >= 1) + { + m_ohci1_scan_enable = command[3]; + DEBUG_LOG_FMT(IOS_WIIMOTE, "Starlet Bluetooth scan enable set to {:#04x}", + m_ohci1_scan_enable); + } + break; + default: + break; + } + + QueueOHCI1HCICommandComplete(opcode, reply); +} + +StarletMemory::OHCITransferResult StarletMemory::ExecuteOHCITransfer(size_t controller, + u32 endpoint, u32 direction, + std::vector* buffer, + size_t* actual_length) +{ + *actual_length = 0; + if (controller != 1) + return OHCITransferResult::Stalled; + + if (endpoint != 0) + { + if (m_ohci1_configuration == 0) + return OHCITransferResult::Stalled; + if (endpoint == 1 && direction == OHCI_DIRECTION_IN) + { + if (m_ohci1_hci_events.empty()) + return OHCITransferResult::Pending; + + std::vector& event = m_ohci1_hci_events.front(); + *actual_length = std::min(buffer->size(), event.size()); + std::copy_n(event.begin(), *actual_length, buffer->begin()); + if (*actual_length == event.size()) + { + if (event.front() == HCI_EVENT_CON_COMPL) + m_ohci1_acl_delay_frames = 2; + m_ohci1_hci_events.pop_front(); + } + else + event.erase(event.begin(), event.begin() + static_cast(*actual_length)); + return OHCITransferResult::Completed; + } + + if (endpoint == 2 && direction == OHCI_DIRECTION_IN) + { + // The Bluetooth controller cannot deliver L2CAP data before its + // connection-complete event. OHCI services bulk before periodic + // endpoints, so also leave one complete USB frame for IOS to consume that + // event before making the first ACL packet visible. + const bool connection_complete_pending = + std::ranges::any_of(m_ohci1_hci_events, [](const std::vector& event) { + return !event.empty() && event.front() == HCI_EVENT_CON_COMPL; + }); + if (connection_complete_pending || m_ohci1_acl_delay_frames != 0 || + m_ohci1_acl_packets.empty()) + return OHCITransferResult::Pending; + + std::vector& packet = m_ohci1_acl_packets.front(); + *actual_length = std::min(buffer->size(), packet.size()); + std::copy_n(packet.begin(), *actual_length, buffer->begin()); + if (*actual_length == packet.size()) + m_ohci1_acl_packets.pop_front(); + else + packet.erase(packet.begin(), packet.begin() + static_cast(*actual_length)); + return OHCITransferResult::Completed; + } + + if (endpoint == 2 && direction == OHCI_DIRECTION_OUT) + { + *actual_length = buffer->size(); + ExecuteOHCI1ACLPacket(buffer); + return OHCITransferResult::Completed; + } + return OHCITransferResult::Stalled; + } + + if (direction == OHCI_DIRECTION_SETUP) + { + if (buffer->size() < m_ohci1_setup_packet.size()) + return OHCITransferResult::Stalled; + std::copy_n(buffer->begin(), m_ohci1_setup_packet.size(), m_ohci1_setup_packet.begin()); + m_ohci1_control_response.clear(); + m_ohci1_control_request_valid = true; + m_ohci1_control_stalled = false; + m_ohci1_hci_command_pending = false; + const u8 request_type = m_ohci1_setup_packet[0]; + const u8 request = m_ohci1_setup_packet[1]; + const u16 value = static_cast(m_ohci1_setup_packet[2] | + (static_cast(m_ohci1_setup_packet[3]) << 8)); + const u16 length = static_cast(m_ohci1_setup_packet[6] | + (static_cast(m_ohci1_setup_packet[7]) << 8)); + DEBUG_LOG_FMT(IOS_USB, + "Starlet OHCI1 control request type {:#04x}, request {:#04x}, " + "value {:#06x}, length {}", + request_type, request, value, length); + + if ((request_type & 0x60) == 0) + { + switch (request) + { + case USB_REQUEST_GET_STATUS: + m_ohci1_control_response = {0, 0}; + break; + case USB_REQUEST_SET_ADDRESS: + m_ohci1_pending_address = static_cast(value & 0x7f); + break; + case USB_REQUEST_GET_DESCRIPTOR: + m_ohci1_control_response = GetOHCI1Descriptor(value); + if (m_ohci1_control_response.empty()) + m_ohci1_control_stalled = true; + break; + case USB_REQUEST_GET_CONFIGURATION: + m_ohci1_control_response = {m_ohci1_configuration}; + break; + case USB_REQUEST_SET_CONFIGURATION: + m_ohci1_configuration = static_cast(value); + break; + case USB_REQUEST_GET_INTERFACE: + m_ohci1_control_response = {0}; + break; + case USB_REQUEST_SET_INTERFACE: + break; + default: + m_ohci1_control_stalled = true; + break; + } + } + else if ((request_type & 0x60) == 0x20 && request == 0) + { + m_ohci1_hci_command_pending = true; + } + else + { + m_ohci1_control_stalled = true; + } + if (m_ohci1_control_response.size() > length) + m_ohci1_control_response.resize(length); + *actual_length = m_ohci1_setup_packet.size(); + return m_ohci1_control_stalled ? OHCITransferResult::Stalled : OHCITransferResult::Completed; + } + + if (!m_ohci1_control_request_valid || m_ohci1_control_stalled) + return OHCITransferResult::Stalled; + + if (direction == OHCI_DIRECTION_IN && !buffer->empty()) + { + *actual_length = std::min(buffer->size(), m_ohci1_control_response.size()); + std::copy_n(m_ohci1_control_response.begin(), *actual_length, buffer->begin()); + } + else if (direction == OHCI_DIRECTION_OUT) + { + *actual_length = buffer->size(); + if (m_ohci1_hci_command_pending && !buffer->empty()) + { + ExecuteOHCI1HCICommand(*buffer); + m_ohci1_hci_command_pending = false; + } + } + + if (buffer->empty()) + { + if (m_ohci1_pending_address != 0xff) + { + m_ohci1_device_address = m_ohci1_pending_address; + m_ohci1_pending_address = 0xff; + } + m_ohci1_control_request_valid = false; + m_ohci1_control_response.clear(); + } + return OHCITransferResult::Completed; +} + +bool StarletMemory::ProcessOHCIEndpoint(size_t controller, u32 endpoint_address, + bool* interrupt_due) +{ + const u32 base = OHCI_BASES[controller]; + const u32 endpoint_flags = ReadOHCIMemory32(endpoint_address); + if ((endpoint_flags & (OHCI_ENDPOINT_SKIP | OHCI_ENDPOINT_ISOCHRONOUS)) != 0) + return false; + + const u32 endpoint = (endpoint_flags >> 7) & 0xf; + const u32 endpoint_direction = (endpoint_flags >> 11) & 3; + const u32 function_address = endpoint_flags & 0x7f; + const u32 tail = ReadOHCIMemory32(endpoint_address + 4) & ~0xfU; + u32 head_word = ReadOHCIMemory32(endpoint_address + 8); + u32 head = head_word & ~0xfU; + if ((head_word & OHCI_ENDPOINT_HALTED) != 0 || head == tail) + return false; + + bool completed_any = false; + for (size_t count = 0; count < 64 && head != 0 && head != tail; ++count) + { + const u32 td_flags = ReadOHCIMemory32(head); + const u32 current_buffer = ReadOHCIMemory32(head + 4); + const u32 next_td = ReadOHCIMemory32(head + 8) & ~0xfU; + const u32 buffer_end = ReadOHCIMemory32(head + 12); + const u32 td_direction = (td_flags >> OHCI_TD_DIRECTION_SHIFT) & 3; + const u32 direction = endpoint_direction == 0 ? td_direction : endpoint_direction; + std::vector buffer = ReadOHCIBuffer(current_buffer, buffer_end); + size_t actual_length = 0; + + OHCITransferResult result = OHCITransferResult::Stalled; + if (controller == 1 && function_address == m_ohci1_device_address && direction != 3) + result = ExecuteOHCITransfer(controller, endpoint, direction, &buffer, &actual_length); + else if (controller == 1 && direction != 3) + result = OHCITransferResult::Stalled; + + if (result == OHCITransferResult::Pending) + break; + + u32 condition_code = OHCI_CC_NO_ERROR; + if (result == OHCITransferResult::Stalled) + condition_code = function_address == m_ohci1_device_address ? OHCI_CC_STALL : + OHCI_CC_DEVICE_NOT_RESPONDING; + // OHCI uses the TD's current-buffer pointer to report how much of the + // requested buffer was left after a short packet. Clearing it means the + // entire buffer was transferred, which makes IOS pass the buffer capacity + // (rather than the USB packet length) to clients such as WPAD. + u32 remaining_buffer = current_buffer; + if (result == OHCITransferResult::Completed) + { + actual_length = std::min(actual_length, buffer.size()); + if (direction == OHCI_DIRECTION_IN) + WriteOHCIBuffer(current_buffer, buffer_end, buffer.data(), actual_length); + if (actual_length == buffer.size()) + { + remaining_buffer = 0; + } + else if (current_buffer != 0 && actual_length != 0) + { + const bool same_page = (current_buffer & ~0xfffU) == (buffer_end & ~0xfffU); + const size_t first_capacity = + same_page ? buffer_end - current_buffer + 1 : 0x1000 - (current_buffer & 0xfffU); + if (same_page || actual_length < first_capacity) + remaining_buffer = current_buffer + static_cast(actual_length); + else + remaining_buffer = + (buffer_end & ~0xfffU) + static_cast(actual_length - first_capacity); + } + } + + WriteOHCIMemory32(head, (td_flags & ~OHCI_TD_CONDITION_CODE_MASK) | + (condition_code << OHCI_TD_CONDITION_CODE_SHIFT)); + WriteOHCIMemory32(head + 4, remaining_buffer); + const u32 previous_done = ReadRegister(base + OHCI_DONE_HEAD); + WriteOHCIMemory32(head + 8, previous_done & ~0xfU); + WriteRegister(base + OHCI_DONE_HEAD, head | (previous_done & 1)); + DEBUG_LOG_FMT(IOS_USB, + "Starlet OHCI{} completed TD {:#010x}, endpoint {}, direction {}, " + "length {}, condition {}", + controller, head, endpoint, direction, actual_length, condition_code); + head_word = next_td | (head_word & 2); + if (result == OHCITransferResult::Stalled) + head_word |= OHCI_ENDPOINT_HALTED; + WriteOHCIMemory32(endpoint_address + 8, head_word); + completed_any = true; + if (((td_flags >> OHCI_TD_INTERRUPT_DELAY_SHIFT) & 7) != 7) + *interrupt_due = true; + if (result == OHCITransferResult::Stalled) + break; + head = next_td; + } + return completed_any; +} + +void StarletMemory::ProcessOHCISchedules(size_t controller) +{ + if (!m_ohci_operational[controller]) + return; + + const u32 base = OHCI_BASES[controller]; + const u32 control = ReadRegister(base + OHCI_CONTROL); + bool completed_any = false; + bool interrupt_due = false; + const auto process_list = [&](u32 head_register, u32 current_register) { + u32 endpoint = ReadRegister(base + current_register) & ~0xfU; + if (endpoint == 0) + endpoint = ReadRegister(base + head_register) & ~0xfU; + for (size_t count = 0; count < 64 && endpoint != 0; ++count) + { + WriteRegister(base + current_register, endpoint); + const u32 next_endpoint = ReadOHCIMemory32(endpoint + 12) & ~0xfU; + completed_any |= ProcessOHCIEndpoint(controller, endpoint, &interrupt_due); + endpoint = next_endpoint; + } + WriteRegister(base + current_register, 0); + }; + + u32 command_status = ReadRegister(base + OHCI_COMMAND_STATUS); + if ((control & OHCI_CONTROL_CONTROL_LIST_ENABLE) != 0) + { + process_list(OHCI_CONTROL_HEAD_ED, OHCI_CONTROL_CURRENT_ED); + command_status &= ~OHCI_COMMAND_CONTROL_LIST_FILLED; + } + if ((control & OHCI_CONTROL_BULK_LIST_ENABLE) != 0) + { + process_list(OHCI_BULK_HEAD_ED, OHCI_BULK_CURRENT_ED); + command_status &= ~OHCI_COMMAND_BULK_LIST_FILLED; + } + WriteRegister(base + OHCI_COMMAND_STATUS, command_status); + + if ((control & OHCI_CONTROL_PERIODIC_LIST_ENABLE) != 0) + { + const u32 hcca = ReadRegister(base + OHCI_HCCA); + if (hcca != 0) + { + const u32 frame = ReadRegister(base + OHCI_FRAME_NUMBER) & 0xffff; + u32 endpoint = ReadOHCIMemory32(hcca + (frame & 31) * 4) & ~0xfU; + for (size_t count = 0; count < 64 && endpoint != 0; ++count) + { + const u32 next_endpoint = ReadOHCIMemory32(endpoint + 12) & ~0xfU; + completed_any |= ProcessOHCIEndpoint(controller, endpoint, &interrupt_due); + endpoint = next_endpoint; + } + } + } + + if (completed_any && interrupt_due) + WriteRegister(base + OHCI_DONE_HEAD, ReadRegister(base + OHCI_DONE_HEAD) | 1); + + // HcDoneHead is an internal queue. The controller must not overwrite + // HCCA.DoneHead while WDH is still pending, otherwise IOS loses completed + // transfer descriptors under sustained HID traffic and stops rearming the + // Bluetooth endpoints. Bit 0 is free because TDs are 16-byte aligned; keep + // the pending interrupt-delay state there until the queue can be published. + const u32 done_head = ReadRegister(base + OHCI_DONE_HEAD); + const bool writeback_pending = + (ReadRegister(base + OHCI_INTERRUPT_STATUS) & OHCI_INTERRUPT_WRITEBACK_DONE_HEAD) != 0; + if ((done_head & ~0xfU) != 0 && (done_head & 1) != 0 && !writeback_pending) + { + const u32 hcca = ReadRegister(base + OHCI_HCCA); + if (hcca != 0) + WriteOHCIMemory32(hcca + 0x84, done_head & ~0xfU); + WriteRegister(base + OHCI_DONE_HEAD, 0); + SetOHCIInterruptStatus(controller, OHCI_INTERRUPT_WRITEBACK_DONE_HEAD); + } +} + +u32 StarletMemory::ReadSDHCRegister(u32 address) const +{ + switch (address) + { + case SDHC_PRESENT_STATE: + // The controller exists even when Dolphin has no SD image attached. + // Reporting a stable empty slot lets the original IOS SDI module initialize + // instead of hanging on controller reset. + return SDHC_CARD_STATE_STABLE | + (m_sd_card_inserted ? + SDHC_CARD_INSERTED | SDHC_CARD_DETECT_PIN_LEVEL | + (Config::Get(Config::MAIN_ALLOW_SD_WRITES) ? SDHC_WRITE_PROTECT_SWITCH : 0) | + SDHC_CARD_SIGNAL_LEVELS : + 0); + case SDHC_CAPABILITIES: + return SDHC_CAPABILITIES_VALUE; + case SDHC_MAX_CAPABILITIES: + return 0; + case SDHC_SLOT_AND_VERSION: + return SDHC_VERSION_VALUE; + default: + return ReadRegister(address); + } +} + +u32 StarletMemory::ReadWiFiSDIORegister(u32 address) const +{ + const u32 offset = GetWiFiSDIOOffset(address); + switch (offset) + { + case SDHC_PRESENT_STATE - SDHC_BASE: + // The Wii's soldered-down Broadcom WLAN device is permanently attached to + // the second SDHCI controller. WL waits for both the stable and inserted + // bits before resetting the host. + return SDHC_CARD_STATE_STABLE | SDHC_CARD_INSERTED | SDHC_CARD_DETECT_PIN_LEVEL | + SDHC_CARD_SIGNAL_LEVELS; + case SDHC_CAPABILITIES - SDHC_BASE: + return WIFI_SDIO_CAPABILITIES_VALUE; + case SDHC_MAX_CAPABILITIES - SDHC_BASE: + return 0; + case SDHC_SLOT_AND_VERSION - SDHC_BASE: + return SDHC_VERSION_VALUE; + default: + // Canonicalize the trusted alias so both mappings observe one controller + // register file. + return ReadRegister(WIFI_SDIO_BASE + offset); + } +} + +void StarletMemory::HandleWiFiSDIOWrite(u32 address) +{ + const u32 offset = GetWiFiSDIOOffset(address); + const u32 canonical_address = WIFI_SDIO_BASE + offset; + if (offset == SDHC_CLOCK_CONTROL - SDHC_BASE) + { + u32 value = ReadRegister(canonical_address); + if ((value & SDHC_INTERNAL_CLOCK_ENABLE) != 0) + value |= SDHC_INTERNAL_CLOCK_STABLE; + else + value &= ~SDHC_INTERNAL_CLOCK_STABLE; + if ((value & SDHC_SOFTWARE_RESET_MASK) != 0) + WriteRegister(WIFI_SDIO_BASE + SDHC_INTERRUPT_STATUS - SDHC_BASE, 0); + value &= ~SDHC_SOFTWARE_RESET_MASK; + WriteRegister(canonical_address, value); + UpdateWiFiSDIOInterrupt(); + } + else if (offset == SDHC_INTERRUPT_STATUS - SDHC_BASE) + { + WriteRegister(canonical_address, + m_wifi_sdio_status_before_write & ~ReadRegister(canonical_address)); + UpdateWiFiSDIOInterrupt(); + } + else if (offset == SDHC_INTERRUPT_STATUS_ENABLE - SDHC_BASE || + offset == SDHC_INTERRUPT_SIGNAL_ENABLE - SDHC_BASE) + { + UpdateWiFiSDIOInterrupt(); + } + else if (offset == SDHC_TRANSFER_MODE_AND_COMMAND - SDHC_BASE) + { + ExecuteWiFiSDIOCommand(ReadRegister(canonical_address)); + } +} + +u8 StarletMemory::ReadWiFiSDIOByte(u32 function, u32 address) const +{ + if (function == 0) + { + // The three CIS pointers are little-endian 24-bit addresses in the common + // register space. Nintendo's Broadcom host stack reads 512 bytes from every + // advertised function, so keep the short tuple chains in separate + // 0x200-byte windows. + if ((address >= 0x09 && address <= 0x0b) || (address >= 0x109 && address <= 0x10b) || + (address >= 0x209 && address <= 0x20b)) + { + const u32 base = address < 0x100 ? 0x1000 : (address < 0x200 ? 0x1200 : 0x1400); + const u32 pointer_byte = (address & 0xff) - 0x09; + return static_cast(base >> (pointer_byte * 8)); + } + + // Minimal production-card CIS data. The standard MANFID tuple identifies + // the Wii WLAN daughterboard; the Broadcom HNBU tuples identify the + // underlying BCM4318 to the original WL module and provide conservative + // board defaults. Unused bytes are CISTPL_NULL and each chain is + // explicitly terminated by CISTPL_END. + static constexpr std::array common_cis = { + 0x20, 0x04, 0xd0, 0x02, 0x4b, 0x04, // Broadcom / Nintendo Wii WLAN. + 0x21, 0x02, 0x0c, 0x00, // SDIO function. + 0x22, 0x04, 0x00, 0x00, 0x02, 0x32, // 512-byte blocks, 25 MHz. + 0xff}; + static constexpr std::array function_cis = { + 0x20, 0x04, 0xd0, 0x02, 0x4b, 0x04, // SDIO MANFID. + 0x21, 0x02, 0x0c, 0x00, // SDIO function. + 0x22, 0x08, 0x04, 0x06, 0x02, 0x00, 0x00, 0x43, 0x18, 0x01, + // FUNCE LAN node ID. + 0x80, 0x02, 0x00, 0x03, // HNBU: SROM revision 3. + 0x80, 0x05, 0x01, 0xe4, 0x14, 0x18, 0x43, // HNBU: BCM4318. + 0x80, 0x02, 0x02, 0x10, // HNBU: board revision. + 0x80, 0x02, 0x06, 0x01, // HNBU: one antenna available. + 0x80, 0x07, 0x19, 0x02, 0x00, 0x00, 0x43, 0x18, + 0x01, // HNBU: local MAC. + 0x80, 0x03, 0x1b, 0x4b, 0x04, // HNBU: Wii board type. + 0xff}; + if (address >= 0x1000 && address < 0x1000 + common_cis.size()) + return common_cis[address - 0x1000]; + const auto read_function_cis = [](size_t offset) { + // HNBU_MACADDR starts at byte 42. Use the same configurable address as + // IOS networking so exploits which derive a key from the console MAC + // observe one coherent piece of hardware. + constexpr size_t mac_offset = 42; + if (offset >= mac_offset && offset < mac_offset + Common::MAC_ADDRESS_SIZE) + return IOS::Net::GetMACAddress()[offset - mac_offset]; + return function_cis[offset]; + }; + if (address >= 0x1200 && address < 0x1200 + function_cis.size()) + return read_function_cis(address - 0x1200); + if (address >= 0x1400 && address < 0x1400 + function_cis.size()) + return read_function_cis(address - 0x1400); + + switch (address) + { + case 0x00: // CCCR/SDIO revision 3.0/2.0 + return 0x32; + case 0x01: // SD physical-layer specification 2.0 + return 0x02; + case 0x03: // I/O ready follows I/O enable in this untimed device + return ReadWiFiSDIOByte(0, 0x02); + case 0x08: // direct commands during data transfer, multi-block and + // low-speed 4-bit bus + return 0xdc; + case 0x13: // high-speed supported and enabled when requested + return static_cast( + 0x01 | + (m_wifi_sdio_registers.contains(0x13) ? m_wifi_sdio_registers.at(0x13) & 0x02 : 0)); + default: + break; + } + } + + if (function == 1 && address < 0x10000) + { + const u32 backplane_address = GetWiFiSDIOBackplaneAddress(address); + const auto override = m_wifi_sdio_registers.find(0x80000000U | backplane_address); + if (override != m_wifi_sdio_registers.end()) + return override->second; + + // The Wii WLAN daughterboard contains a BCM4318 on a Sonics Silicon + // Backplane. WL starts by enumerating the four 4 KiB cores through the + // function-1 backplane aperture. The ID words below describe the + // production BCM4318 core set used by the original Broadcom driver. + const u32 word_address = backplane_address & ~3U; + u32 value = 0; + switch (word_address) + { + case 0x18000000: // ChipCommon chip ID: four cores, revision 2, BCM4318. + value = 0x04024318; + break; + case 0x18000ff8: + case 0x18001ff8: + case 0x18002ff8: + case 0x18003ff8: + value = 0x10000000; // Sonics backplane revision 2.3. + break; + case 0x18000ffc: // ChipCommon, revision 13. + value = 0x4243800d; + break; + case 0x18001ffc: // IEEE 802.11, revision 9. + value = 0x42438129; + break; + case 0x18001120: // D11 maccontrol: IHR access enabled after reset. + value = 0x00000400; + break; + case 0x180013e0: // D11 PHY version: analog 3, G-PHY type 2, revision 7. + value = 0x32070000; + break; + case 0x180013f8: // D11 radio ID high word: BCM2050. + value = 0x02050000; + break; + case 0x18002ffc: // PCI bridge, revision 12. + value = 0x4243804c; + break; + case 0x18003ffc: // PCMCIA/SDIO bridge, revision 7. + value = 0x424380d7; + break; + default: + break; + } + // IOS runs on a big-endian ARM core and consumes backplane words directly. + // Present the bytes in bus order so a 32-bit load observes the documented + // Sonics/BCM register value (for example 0x4243800d for IDHIGH). + const u8 result = static_cast(value >> (24 - (backplane_address & 3) * 8)); + return result; + } + + const u32 key = (function << 17) | (address & 0x1ffff); + const auto it = m_wifi_sdio_registers.find(key); + if (function == 1 && address == 0x1000e) + { + // Clock requests complete immediately. BCM4318 exposes ALP and HT + // availability in the same byte as the request bits, which WL polls after + // each change. + const u8 requested = it == m_wifi_sdio_registers.end() ? 0 : it->second; + return static_cast(requested | 0x40 | ((requested & 0x10) != 0 ? 0x80 : 0)); + } + const u8 result = it == m_wifi_sdio_registers.end() ? 0 : it->second; + return result; +} + +void StarletMemory::WriteWiFiSDIOByte(u32 function, u32 address, u8 value) +{ + address &= 0x1ffff; + if (function == 1 && address < 0x10000) + { + const u32 backplane_address = GetWiFiSDIOBackplaneAddress(address); + m_wifi_sdio_registers[0x80000000U | backplane_address] = value; + + // D11's 32-bit register window and its 16-bit IHR window expose the same + // CFPStart register. The older Broadcom driver shipped in IOS80 + // deliberately writes 0xccccbbbb through the 32-bit window and expects + // 0xbbbb/0xcccc from the two 16-bit aliases as its bus-width test. ARM is + // big-endian, so exchanging the two halfwords maps the byte lanes onto + // those aliases. + constexpr u32 d11_cfpstart = 0x1800118c; + constexpr u32 d11_cfpstart_ihr = 0x18001604; + if (backplane_address >= d11_cfpstart && backplane_address < d11_cfpstart + 4) + { + const u32 lane = backplane_address - d11_cfpstart; + const u32 alias_address = d11_cfpstart_ihr + (lane ^ 2); + m_wifi_sdio_registers[0x80000000U | alias_address] = value; + } + return; + } + if (function == 0 && address == 0x06 && (value & 0x08) != 0) + { + // CCCR I/O_ABORT bit 3 resets all I/O functions while retaining the common + // register file. + std::erase_if(m_wifi_sdio_registers, + [](const auto& entry) { return (entry.first >> 17) != 0; }); + m_wifi_sdio_selected = false; + return; + } + m_wifi_sdio_registers[(function << 17) | address] = value; +} + +u32 StarletMemory::GetWiFiSDIOBackplaneAddress(u32 address) const +{ + const auto get_window_byte = [this](u32 register_address, u8 default_value) { + const auto it = m_wifi_sdio_registers.find((1U << 17) | register_address); + return it == m_wifi_sdio_registers.end() ? default_value : it->second; + }; + const u32 low = get_window_byte(0x1000a, 0x00); + const u32 mid = get_window_byte(0x1000b, 0x00); + const u32 high = get_window_byte(0x1000c, 0x18); + return (high << 24) | (mid << 16) | (low << 8) | (address & 0x7fff); +} + +void StarletMemory::SetWiFiSDIOInterruptStatus(u16 normal_status, u16 error_status) +{ + const u32 status_address = WIFI_SDIO_BASE + SDHC_INTERRUPT_STATUS - SDHC_BASE; + const u32 status = + ReadRegister(status_address) | normal_status | (static_cast(error_status) << 16); + WriteRegister(status_address, status); + UpdateWiFiSDIOInterrupt(); +} + +void StarletMemory::ExecuteWiFiSDIOCommand(u32 command_and_mode) +{ + const u16 transfer_mode = static_cast(command_and_mode); + const u16 command = static_cast(command_and_mode >> 16); + const u32 command_index = (command >> 8) & 0x3f; + const u32 argument_address = WIFI_SDIO_BASE + SDHC_ARGUMENT - SDHC_BASE; + const u32 response_address = WIFI_SDIO_BASE + SDHC_RESPONSE - SDHC_BASE; + const u32 dma_register = WIFI_SDIO_BASE + SDHC_DMA_ADDRESS - SDHC_BASE; + const u32 block_register = WIFI_SDIO_BASE + SDHC_BLOCK_SIZE_AND_COUNT - SDHC_BASE; + const u32 argument = ReadRegister(argument_address); + WriteRegister(response_address, 0); + + bool succeeded = true; + bool transfer_complete = false; + switch (command_index) + { + case SD_CARD_GO_IDLE: + m_wifi_sdio_selected = false; + break; + case SD_IO_SEND_OP_CONDITION: + // R4: ready, two I/O functions, no memory function, and the Wii's 3.2-3.4 V + // range. + WriteRegister(response_address, 0xa0ff8000); + break; + case SD_CARD_SEND_RELATIVE_ADDRESS: + WriteRegister(response_address, static_cast(SD_CARD_RELATIVE_ADDRESS) << 16); + break; + case SD_CARD_SELECT: + m_wifi_sdio_selected = (argument >> 16) == SD_CARD_RELATIVE_ADDRESS; + // R1: the selected I/O card is in TRAN state, ready for data, and has + // completed selection. IOS's Broadcom-derived host driver compares this + // status word exactly. + WriteRegister(response_address, m_wifi_sdio_selected ? 0x1e00 : 0); + break; + case SD_IO_RW_DIRECT: + { + const bool write = (argument & (1U << 31)) != 0; + const bool read_after_write = (argument & (1U << 27)) != 0; + const u32 function = (argument >> 28) & 7; + const u32 register_address = (argument >> 9) & 0x1ffff; + u8 data = static_cast(argument); + if (function > 2) + { + succeeded = false; + break; + } + if (write) + WriteWiFiSDIOByte(function, register_address, data); + if (!write || read_after_write) + data = ReadWiFiSDIOByte(function, register_address); + // R5's I/O-current-state field must report the command state (1). + // Nintendo's WL host code, like the Broadcom bcmsdstd implementation it + // derives from, rejects a response unless the upper response-flags byte is + // 0x10 even when all error bits are clear. + WriteRegister(response_address, 0x1000 | data); + break; + } + case SD_IO_RW_EXTENDED: + { + const bool write = (argument & (1U << 31)) != 0; + const u32 function = (argument >> 28) & 7; + const bool block_mode = (argument & (1U << 27)) != 0; + const bool increment = (argument & (1U << 26)) != 0; + u32 register_address = (argument >> 9) & 0x1ffff; + u32 count = argument & 0x1ff; + if (count == 0) + count = 512; + const u32 block_size_and_count = ReadRegister(block_register); + const u32 block_size = block_size_and_count & 0xfff; + u32 size = block_mode ? count * block_size : count; + const u32 dma_address = ReadRegister(dma_register); + if (function > 2 || size == 0 || size > 0x100000) + { + succeeded = false; + break; + } + + if ((transfer_mode & SDHC_TRANSFER_DMA_ENABLE) != 0) + { + for (u32 i = 0; i < size; ++i) + { + if (write) + WriteWiFiSDIOByte(function, register_address, ReadMapped8(dma_address + i)); + else + WriteMapped8(dma_address + i, ReadWiFiSDIOByte(function, register_address)); + if (increment) + register_address = (register_address + 1) & 0x1ffff; + } + WriteRegister(dma_register, dma_address + size); + transfer_complete = true; + } + else if (write) + { + // Nintendo's WL module uses the SDHCI buffer-data port for its short + // backplane accesses. Arm the FIFO here; individual byte writes to the + // 32-bit port complete the transaction. + m_wifi_sdio_pio_read_data.clear(); + m_wifi_sdio_pio_write_function = function; + m_wifi_sdio_pio_write_address = register_address; + m_wifi_sdio_pio_write_remaining = size; + m_wifi_sdio_pio_write_increment = increment; + SetWiFiSDIOInterruptStatus(SDHC_INTERRUPT_BUFFER_WRITE_READY); + } + else + { + m_wifi_sdio_pio_read_data.clear(); + for (u32 i = 0; i < size; ++i) + { + m_wifi_sdio_pio_read_data.push_back(ReadWiFiSDIOByte(function, register_address)); + if (increment) + register_address = (register_address + 1) & 0x1ffff; + } + m_wifi_sdio_pio_write_remaining = 0; + SetWiFiSDIOInterruptStatus(SDHC_INTERRUPT_BUFFER_READ_READY); + } + WriteRegister(response_address, 0x1000); + break; + } + default: + succeeded = false; + break; + } + + if (!succeeded) + { + WARN_LOG_FMT(IOS_SD, "Unsupported Starlet WiFi SDIO command {}", command_index); + SetWiFiSDIOInterruptStatus(SDHC_INTERRUPT_ERROR, (command & SDHC_COMMAND_DATA_PRESENT) != 0 ? + SDHC_ERROR_DATA_TIMEOUT : + SDHC_ERROR_COMMAND_TIMEOUT); + return; + } + SetWiFiSDIOInterruptStatus(SDHC_INTERRUPT_COMMAND_COMPLETE | + (transfer_complete ? SDHC_INTERRUPT_TRANSFER_COMPLETE : 0)); +} + +void StarletMemory::UpdateWiFiSDIOInterrupt() +{ + const u32 status = WIFI_SDIO_BASE + SDHC_INTERRUPT_STATUS - SDHC_BASE; + const u32 status_enable = WIFI_SDIO_BASE + SDHC_INTERRUPT_STATUS_ENABLE - SDHC_BASE; + const u32 signal_enable = WIFI_SDIO_BASE + SDHC_INTERRUPT_SIGNAL_ENABLE - SDHC_BASE; + const u32 pending = + ReadRegister(status) & ReadRegister(status_enable) & ReadRegister(signal_enable); + m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_WIFI, pending != 0); +} + +void StarletMemory::HandleSDHCWrite(u32 address) +{ + if (address == SDHC_CLOCK_CONTROL) + { + u32 value = ReadRegister(address); + // Clock stabilization and a software reset complete immediately in this + // untimed controller shell. The low half is CLOCK_CONTROL; the reset byte + // is the high byte because the Wii block exposes the standard SDHCI + // register file with reversed little-endian byte lanes. + if ((value & SDHC_INTERNAL_CLOCK_ENABLE) != 0) + value |= SDHC_INTERNAL_CLOCK_STABLE; + else + value &= ~SDHC_INTERNAL_CLOCK_STABLE; + if ((value & SDHC_SOFTWARE_RESET_MASK) != 0) + { + m_sd_app_command = false; + m_sd_card_selected = false; + WriteRegister(SDHC_INTERRUPT_STATUS, 0); + } + value &= ~SDHC_SOFTWARE_RESET_MASK; + WriteRegister(address, value); + UpdateSDHCInterrupt(); + } + else if (address == SDHC_INTERRUPT_STATUS) + { + // Both normal and error status halves are write-one-to-clear. + WriteRegister(address, m_sdhc_status_before_write & ~ReadRegister(address)); + UpdateSDHCInterrupt(); + } + else if (address == SDHC_INTERRUPT_STATUS_ENABLE || address == SDHC_INTERRUPT_SIGNAL_ENABLE) + { + UpdateSDHCInterrupt(); + } + else if (address == SDHC_TRANSFER_MODE_AND_COMMAND) + { + ExecuteSDHCCommand(ReadRegister(address)); + } +} + +void StarletMemory::SetSDHCInterruptStatus(u16 normal_status, u16 error_status) +{ + u32 status = ReadRegister(SDHC_INTERRUPT_STATUS); + status |= normal_status; + status |= static_cast(error_status) << 16; + WriteRegister(SDHC_INTERRUPT_STATUS, status); + UpdateSDHCInterrupt(); +} + +void StarletMemory::UpdateSDHCInterrupt() +{ + const u32 status = ReadRegister(SDHC_INTERRUPT_STATUS); + const u32 status_enable = ReadRegister(SDHC_INTERRUPT_STATUS_ENABLE); + const u32 signal_enable = ReadRegister(SDHC_INTERRUPT_SIGNAL_ENABLE); + const u32 pending = status & status_enable & signal_enable; + m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_SD, pending != 0); +} + +std::array StarletMemory::GetSDCardCSD() const +{ + const u64 card_size = m_sd_card.GetSize(); + if (m_sd_card_high_capacity) + { + const u32 c_size = card_size >= 512 * 1024 ? static_cast(card_size / (512 * 1024) - 1) : 0; + return {0x00400e00, 0x5a5f5900, 0x0000007f | (c_size << 8), 0x800a4000}; + } + + u64 units = std::max(card_size, 1); + u32 read_block_length = 9; + u32 size_multiplier = 0; + while (units > 4096 && size_multiplier <= 7 + 2 + read_block_length) + { + units >>= 1; + ++size_multiplier; + } + if (size_multiplier > 7 + 2 + read_block_length && read_block_length < 15) + ++read_block_length; + size_multiplier = + size_multiplier > 2 + read_block_length ? size_multiplier - 2 - read_block_length : 0; + const u32 c_size = static_cast(std::min(units - 1, 0xfff)); + return {0x000007f0, 0x035b5080 | (read_block_length << 8) | (c_size >> 10), + 0x003ffc7f | (c_size << 22) | (size_multiplier << 7), + 0x80040040 | (read_block_length << 18)}; +} + +bool StarletMemory::TransferSDCardBlocks(bool read, u32 argument, u32 block_size, u32 block_count, + u32 dma_address) +{ + if (!m_sd_card_inserted || !m_sd_card) + return false; + if (block_size == 0) + block_size = m_sd_block_length; + if (block_count == 0) + block_count = 1; + + const u64 size = static_cast(block_size) * block_count; + const u64 offset = m_sd_card_high_capacity ? static_cast(argument) * 512 : argument; + if (size == 0 || size > std::numeric_limits::max() || offset > m_sd_card.GetSize() || + size > m_sd_card.GetSize() - offset) + { + return false; + } + + std::vector data(static_cast(size)); + m_sd_card.ClearError(); + if (!m_sd_card.Seek(static_cast(offset), File::SeekOrigin::Begin)) + return false; + if (read) + { + if (!m_sd_card.ReadBytes(data.data(), data.size())) + return false; + for (size_t i = 0; i < data.size(); ++i) + WriteMapped8(dma_address + static_cast(i), data[i]); + } + else + { + if (!Config::Get(Config::MAIN_ALLOW_SD_WRITES)) + return false; + for (size_t i = 0; i < data.size(); ++i) + data[i] = ReadMapped8(dma_address + static_cast(i)); + if (!m_sd_card.WriteBytes(data.data(), data.size())) + return false; + } + + WriteRegister(SDHC_DMA_ADDRESS, dma_address + static_cast(size)); + WriteRegister(SDHC_BLOCK_SIZE_AND_COUNT, ReadRegister(SDHC_BLOCK_SIZE_AND_COUNT) & 0xffff); + return true; +} + +void StarletMemory::ExecuteSDHCCommand(u32 command_and_mode) +{ + const u16 transfer_mode = static_cast(command_and_mode); + const u16 command = static_cast(command_and_mode >> 16); + const u32 command_index = (command >> 8) & 0x3f; + const u32 argument = ReadRegister(SDHC_ARGUMENT); + const u32 block_size_and_count = ReadRegister(SDHC_BLOCK_SIZE_AND_COUNT); + const u32 block_size = block_size_and_count & 0xfff; + const u32 block_count = + (transfer_mode & SDHC_TRANSFER_BLOCK_COUNT_ENABLE) != 0 ? block_size_and_count >> 16 : 1; + const u32 dma_address = ReadRegister(SDHC_DMA_ADDRESS); + const bool was_app_command = m_sd_app_command; + const bool data_command = (command & SDHC_COMMAND_DATA_PRESENT) != 0; + m_sd_app_command = false; + + for (u32 i = 0; i < 4; ++i) + WriteRegister(SDHC_RESPONSE + i * 4, 0); + + if (!m_sd_card_inserted && command_index != SD_CARD_GO_IDLE) + { + SetSDHCInterruptStatus(SDHC_INTERRUPT_ERROR, SDHC_ERROR_COMMAND_TIMEOUT); + return; + } + + bool transfer_complete = false; + bool succeeded = true; + if (was_app_command) + { + switch (command_index) + { + case SD_CARD_ACMD_SET_BUS_WIDTH: + WriteRegister(SDHC_RESPONSE, 0x920); + break; + case SD_CARD_ACMD_SEND_OP_CONDITION: + WriteRegister(SDHC_RESPONSE, 0x80ff8000 | (m_sd_card_high_capacity ? 0x40000000 : 0)); + break; + case SD_CARD_ACMD_SEND_SCR: + { + if ((transfer_mode & SDHC_TRANSFER_DMA_ENABLE) == 0 || block_size < 8) + { + succeeded = false; + break; + } + constexpr std::array scr = {0x02, 0x05, 0, 0, 0, 0, 0, 0}; + for (size_t i = 0; i < scr.size(); ++i) + WriteMapped8(dma_address + static_cast(i), scr[i]); + WriteRegister(SDHC_DMA_ADDRESS, dma_address + static_cast(scr.size())); + transfer_complete = true; + break; + } + default: + succeeded = false; + break; + } + } + else + { + switch (command_index) + { + case SD_CARD_GO_IDLE: + m_sd_card_selected = false; + m_sd_block_length = 512; + break; + case SD_CARD_ALL_SEND_CID: + case SD_CARD_SEND_CID: + { + constexpr std::array cid = {0x00d0444f, 0x4c504849, 0x4e430403, 0xac68006b}; + for (u32 i = 0; i < cid.size(); ++i) + WriteRegister(SDHC_RESPONSE + i * 4, cid[cid.size() - 1 - i]); + break; + } + case SD_CARD_SEND_RELATIVE_ADDRESS: + WriteRegister(SDHC_RESPONSE, static_cast(SD_CARD_RELATIVE_ADDRESS) << 16); + break; + case SD_CARD_SELECT: + m_sd_card_selected = (argument >> 16) == SD_CARD_RELATIVE_ADDRESS; + WriteRegister(SDHC_RESPONSE, m_sd_card_selected ? 0x700 : 0x900); + break; + case SD_CARD_SEND_IF_CONDITION: + WriteRegister(SDHC_RESPONSE, argument); + break; + case SD_CARD_SEND_CSD: + { + const auto csd = GetSDCardCSD(); + for (u32 i = 0; i < csd.size(); ++i) + WriteRegister(SDHC_RESPONSE + i * 4, csd[csd.size() - 1 - i]); + break; + } + case SD_CARD_STOP_TRANSMISSION: + WriteRegister(SDHC_RESPONSE, 0x900); + break; + case SD_CARD_SEND_STATUS: + WriteRegister(SDHC_RESPONSE, m_sd_card_selected ? 0x900 : 0x700); + break; + case SD_CARD_SET_BLOCK_LENGTH: + m_sd_block_length = argument; + WriteRegister(SDHC_RESPONSE, 0x900); + break; + case SD_CARD_READ_SINGLE: + case SD_CARD_READ_MULTIPLE: + case SD_CARD_WRITE_SINGLE: + case SD_CARD_WRITE_MULTIPLE: + succeeded = (command & SDHC_COMMAND_DATA_PRESENT) != 0 && + (transfer_mode & SDHC_TRANSFER_DMA_ENABLE) != 0 && + TransferSDCardBlocks((transfer_mode & SDHC_TRANSFER_READ) != 0, argument, + block_size, block_count, dma_address); + transfer_complete = succeeded; + WriteRegister(SDHC_RESPONSE, 0x900); + break; + case SD_CARD_APP_COMMAND: + m_sd_app_command = true; + WriteRegister(SDHC_RESPONSE, 0x920); + break; + default: + succeeded = false; + break; + } + } + + if (!succeeded) + { + WARN_LOG_FMT(IOS_SD, "Unsupported or failed Starlet SD command {}{}", command_index, + was_app_command ? " (application)" : ""); + SetSDHCInterruptStatus(SDHC_INTERRUPT_ERROR, + data_command ? SDHC_ERROR_DATA_TIMEOUT : SDHC_ERROR_COMMAND_TIMEOUT); + return; + } + + SetSDHCInterruptStatus(SDHC_INTERRUPT_COMMAND_COMPLETE | + (transfer_complete ? SDHC_INTERRUPT_TRANSFER_COMPLETE : 0)); +} + +bool StarletMemory::IsMemoryControllerIndirectRegister(u32 address) const +{ + const u32 halfword_address = address & ~1U; + return halfword_address == MEM_DDRREG_ADDR || halfword_address == MEM_DDRREG_DATA || + halfword_address == MEM_SEQ_DATA || halfword_address == MEM_SEQ_ADDR || + halfword_address == MEM_BIST_DATA || halfword_address == MEM_BIST_ADDR; +} + +u16 StarletMemory::ReadMemoryControllerHalfword(u32 address) const +{ + switch (address & ~1U) + { + case MEM_DDRREG_ADDR: + return m_ddr_register_address; + case MEM_DDRREG_DATA: + return ReadDDRRegister(m_ddr_register_address); + case MEM_SEQ_DATA: + return m_ddr_seq_registers[m_ddr_seq_address & 0xff]; + case MEM_SEQ_ADDR: + return m_ddr_seq_address; + case MEM_BIST_DATA: + return ReadBISTRegister(m_ddr_bist_address); + case MEM_BIST_ADDR: + return m_ddr_bist_address; + default: + return 0; + } +} + +void StarletMemory::WriteMemoryControllerHalfword(u32 address, u16 value) +{ + switch (address & ~1U) + { + case MEM_DDRREG_ADDR: + m_ddr_register_address = value; + break; + case MEM_DDRREG_DATA: + WriteDDRRegister(m_ddr_register_address, value); + break; + case MEM_SEQ_DATA: + m_ddr_seq_registers[m_ddr_seq_address & 0xff] = value; + break; + case MEM_SEQ_ADDR: + m_ddr_seq_address = value; + break; + case MEM_BIST_DATA: + WriteBISTRegister(m_ddr_bist_address, value); + break; + case MEM_BIST_ADDR: + m_ddr_bist_address = value; + break; + } +} + +u16 StarletMemory::ReadDDRRegister(u16 address) const +{ + switch (address) + { + case DDR_AHMFLUSH_ACK: + return m_ddr_registers[DDR_AHMFLUSH]; + case DDR_SEQ_DATA: + return m_ddr_seq_registers[m_ddr_seq_address & 0xff]; + case DDR_SEQ_ADDR: + return m_ddr_seq_address; + case DDR_BIST_DATA: + return ReadBISTRegister(m_ddr_bist_address); + case DDR_BIST_ADDR: + return m_ddr_bist_address; + default: + return address < m_ddr_registers.size() ? m_ddr_registers[address] : 0; + } +} + +void StarletMemory::WriteDDRRegister(u16 address, u16 value) +{ + if (address < m_ddr_registers.size()) + m_ddr_registers[address] = value; + + switch (address) + { + case DDR_AHMFLUSH: + m_ddr_registers[DDR_AHMFLUSH_ACK] = value; + break; + case DDR_SEQ_DATA: + m_ddr_seq_registers[m_ddr_seq_address & 0xff] = value; + break; + case DDR_SEQ_ADDR: + m_ddr_seq_address = value; + break; + case DDR_BIST_DATA: + WriteBISTRegister(m_ddr_bist_address, value); + break; + case DDR_BIST_ADDR: + m_ddr_bist_address = value; + break; + } +} + +u16 StarletMemory::ReadBISTRegister(u16 address) const +{ + // The host RAM backing is always available, so built-in write/read tests + // complete immediately and report no failing repetitions. + if (address == BIST_WRGO || address == BIST_RDGO) + return 0; + return m_ddr_bist_registers[address & 0xff]; +} + +void StarletMemory::WriteBISTRegister(u16 address, u16 value) +{ + if (address == BIST_WRGO || address == BIST_RDGO) + { + m_ddr_bist_registers[address & 0xff] = 0; + return; + } + m_ddr_bist_registers[address & 0xff] = value; +} + +void StarletMemory::WriteRegister(u32 address, u32 value) +{ + WriteMapped8(address, static_cast(value >> 24)); + WriteMapped8(address + 1, static_cast(value >> 16)); + WriteMapped8(address + 2, static_cast(value >> 8)); + WriteMapped8(address + 3, static_cast(value)); +} + +u8 StarletMemory::Read8(u32 address) +{ + if (IsBootROMAddress(address)) + return m_boot_rom[GetBootROMOffset(address)]; + + if (IsMemoryControllerIndirectRegister(address)) + { + const u16 value = ReadMemoryControllerHalfword(address); + return static_cast(value >> ((address & 1) == 0 ? 8 : 0)); + } + const u32 word_address = address & ~3U; + if (word_address == HW_TIMER) + { + return static_cast(GetTimer() >> (24 - (address & 3) * 8)); + } + if (word_address == HW_GPIO_IN) + { + const u32 value = m_seeprom_miso ? GPIO_EEP_MISO : 0; + return static_cast(value >> (24 - (address & 3) * 8)); + } + if (IsEHCIAddress(word_address)) + { + const u32 value = ReadEHCIRegister(word_address); + const u32 offset = word_address - EHCI_BASE; + if ((address & 3) == 0) + { + const Starlet* const starlet = m_system.GetStarlet(); + DEBUG_LOG_FMT(IOS_USB, "Starlet EHCI read register {:#04x} = {:#010x} at PC {:#010x}", offset, + value, starlet ? starlet->GetPC() : 0); + } + return static_cast(value >> (24 - (address & 3) * 8)); + } + if (const auto controller = GetOHCIControllerIndex(word_address)) + { + const u32 value = ReadOHCIRegister(*controller, word_address); + const u32 offset = word_address - OHCI_BASES[*controller]; + if ((address & 3) == 0 && (offset == OHCI_INTERRUPT_STATUS || offset == OHCI_RH_DESCRIPTOR_A || + offset == OHCI_RH_DESCRIPTOR_B || offset == OHCI_RH_STATUS || + offset == OHCI_RH_PORT_STATUS_1 || offset == OHCI_RH_PORT_STATUS_2)) + { + const Starlet* starlet = m_system.GetStarlet(); + DEBUG_LOG_FMT(IOS_USB, "Starlet OHCI{} read register {:#04x} = {:#010x} at PC {:#010x}", + *controller, offset, value, starlet ? starlet->GetPC() : 0); + } + return static_cast(value >> (24 - (address & 3) * 8)); + } + if (IsDIAddress(word_address)) + { + MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping(); + const u32 value = mmio ? mmio->Read(m_system, word_address) : 0; + return static_cast(value >> (24 - (address & 3) * 8)); + } + if (word_address >= SDHC_BASE && word_address < SDHC_BASE + SDHC_SIZE) + { + const u32 value = ReadSDHCRegister(word_address); + return static_cast(value >> (24 - (address & 3) * 8)); + } + if (IsWiFiSDIOAddress(word_address)) + { + const u32 offset = GetWiFiSDIOOffset(word_address); + if (offset == SDHC_BUFFER_DATA - SDHC_BASE && !m_wifi_sdio_pio_read_data.empty()) + { + // The SDHCI buffer port is 32 bits wide. For a short PIO transfer, IOS + // reads one whole word and the controller places the payload in its + // least-significant byte lanes. + const u32 lane = address & 3; + if (m_wifi_sdio_pio_read_data.size() < 4 && lane < 4 - m_wifi_sdio_pio_read_data.size()) + { + return 0; + } + const u8 value = m_wifi_sdio_pio_read_data.front(); + m_wifi_sdio_pio_read_data.pop_front(); + if (m_wifi_sdio_pio_read_data.empty()) + { + const u32 status_address = WIFI_SDIO_BASE + SDHC_INTERRUPT_STATUS - SDHC_BASE; + WriteRegister(status_address, + ReadRegister(status_address) & ~SDHC_INTERRUPT_BUFFER_READ_READY); + SetWiFiSDIOInterruptStatus(SDHC_INTERRUPT_TRANSFER_COMPLETE); + } + return value; + } + const u32 value = ReadWiFiSDIORegister(word_address); + return static_cast(value >> (24 - (address & 3) * 8)); + } + + if (((word_address >= HW_BASE && word_address <= HW_BASE + 0x0c) || + (word_address >= HW_BASE + 0x30 && word_address <= HW_BASE + 0x40) || + word_address == HW_AHBPROT || word_address == HW_RESETS)) + { + const u32 value = m_system.GetWiiIPC().ReadStarletRegister(word_address - HW_BASE); + return static_cast(value >> (24 - (address & 3) * 8)); + } + return ReadMapped8(address); +} + +u16 StarletMemory::Read16(u32 address) +{ + if (IsMemoryAddress(address) && IsMemoryAddress(address + 1)) + return m_system.GetMemory().Read_U16(address); + + if (IsBootROMAddress(address) && IsBootROMAddress(address + 1)) + { + const u32 offset = GetBootROMOffset(address); + return static_cast((static_cast(m_boot_rom[offset]) << 8) | m_boot_rom[offset + 1]); + } + + if (IsSRAMWindowAddress(address) && IsSRAMWindowAddress(address + 1)) + { + const u32 offset = GetSRAMOffset(address); + const u32 end_offset = GetSRAMOffset(address + 1); + if (offset != INVALID_SRAM_OFFSET && end_offset == offset + 1) + return static_cast((static_cast(m_sram[offset]) << 8) | m_sram[end_offset]); + } + + return ARMBus::Read16(address); +} + +u32 StarletMemory::Read32(u32 address) +{ + if ((address & 3) == 0 && IsDIAddress(address)) + { + MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping(); + return mmio ? mmio->Read(m_system, address) : 0; + } + + if (IsMemoryAddress(address) && IsMemoryAddress(address + 3)) + return m_system.GetMemory().Read_U32(address); + + if (IsBootROMAddress(address) && IsBootROMAddress(address + 3)) + { + const u32 offset = GetBootROMOffset(address); + return (static_cast(m_boot_rom[offset]) << 24) | + (static_cast(m_boot_rom[offset + 1]) << 16) | + (static_cast(m_boot_rom[offset + 2]) << 8) | m_boot_rom[offset + 3]; + } + + if (IsSRAMWindowAddress(address) && IsSRAMWindowAddress(address + 3)) + { + const u32 offset = GetSRAMOffset(address); + const u32 end_offset = GetSRAMOffset(address + 3); + if (offset != INVALID_SRAM_OFFSET && end_offset == offset + 3) + { + return (static_cast(m_sram[offset]) << 24) | + (static_cast(m_sram[offset + 1]) << 16) | + (static_cast(m_sram[offset + 2]) << 8) | m_sram[end_offset]; + } + } + + return ARMBus::Read32(address); +} + +void StarletMemory::Write8(u32 address, u8 value) +{ + if (IsBootROMAddress(address)) + return; + + // NAND_CTRL is normally written a word at a time. Keep its completed value + // while the incoming acknowledgement word is assembled byte by byte, since + // that write is a command rather than a replacement for the readable + // completion status. + if (address == NAND_CTRL) + m_nand_control_before_write = ReadRegister(NAND_CTRL); + if (address == SDHC_INTERRUPT_STATUS) + m_sdhc_status_before_write = ReadRegister(SDHC_INTERRUPT_STATUS); + u32 word_address = address & ~3U; + if (address == HW_USBFRCRST) + m_usb_force_reset_before_write = ReadRegister(HW_USBFRCRST); + const bool wifi_sdio = IsWiFiSDIOAddress(word_address); + if (wifi_sdio) + { + const u32 offset = GetWiFiSDIOOffset(word_address); + word_address = WIFI_SDIO_BASE + offset; + address = word_address + (address & 3); + if (offset == SDHC_BUFFER_DATA - SDHC_BASE && m_wifi_sdio_pio_write_remaining != 0) + { + // Symmetrically, a 1- or 2-byte PIO write takes its payload from the low + // lanes of the 32-bit buffer port. Do not complete the transfer on the + // leading padding bytes. + const u32 lane = address & 3; + if (m_wifi_sdio_pio_write_remaining < 4 && lane < 4 - m_wifi_sdio_pio_write_remaining) + { + return; + } + WriteWiFiSDIOByte(m_wifi_sdio_pio_write_function, m_wifi_sdio_pio_write_address, value); + if (m_wifi_sdio_pio_write_increment) + m_wifi_sdio_pio_write_address = (m_wifi_sdio_pio_write_address + 1) & 0x1ffff; + if (--m_wifi_sdio_pio_write_remaining == 0) + { + const u32 status_address = WIFI_SDIO_BASE + SDHC_INTERRUPT_STATUS - SDHC_BASE; + WriteRegister(status_address, + ReadRegister(status_address) & ~SDHC_INTERRUPT_BUFFER_WRITE_READY); + SetWiFiSDIOInterruptStatus(SDHC_INTERRUPT_TRANSFER_COMPLETE); + } + return; + } + if (offset == SDHC_INTERRUPT_STATUS - SDHC_BASE && (address & 3) == 0) + m_wifi_sdio_status_before_write = ReadRegister(word_address); + } + const auto ohci_controller = GetOHCIControllerIndex(word_address); + const bool ehci = IsEHCIAddress(word_address); + if (IsDIAddress(word_address)) + { + WriteMapped8(address, value); + if ((address & 3) == 3) + { + MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping(); + if (mmio) + mmio->Write(m_system, word_address, ReadRegister(word_address)); + } + return; + } + if (ehci && + (address == EHCI_BASE + EHCI_USB_STATUS || address == EHCI_BASE + EHCI_PORT_STATUS_1 || + address == EHCI_BASE + EHCI_PORT_STATUS_2)) + { + m_ehci_status_before_write = ReadRegister(word_address); + } + if (ohci_controller && address == OHCI_BASES[*ohci_controller] + OHCI_INTERRUPT_STATUS) + { + m_ohci_interrupt_status_before_write[*ohci_controller] = ReadRegister(word_address); + } + if (ohci_controller && (address == OHCI_BASES[*ohci_controller] + OHCI_RH_PORT_STATUS_1 || + address == OHCI_BASES[*ohci_controller] + OHCI_RH_PORT_STATUS_2)) + { + const size_t port = (word_address - OHCI_BASES[*ohci_controller] - OHCI_RH_PORT_STATUS_1) / 4; + m_ohci_port_status_before_write[*ohci_controller][port] = ReadRegister(word_address); + } + WriteMapped8(address, value); + + if (IsMemoryControllerIndirectRegister(address) && (address & 1)) + { + const u32 halfword_address = address & ~1U; + const u16 halfword = static_cast((static_cast(ReadMapped8(halfword_address)) << 8) | + ReadMapped8(halfword_address + 1)); + WriteMemoryControllerHalfword(halfword_address, halfword); + } + + // MEM_AHMFLUSH and MEM_AHMFLUSH_ACK are adjacent 16-bit registers. With no + // host-side cache between Starlet and emulated RAM, a flush completes + // immediately and the acknowledgement mirrors the request. + if (address == MEM_AHMFLUSH + 1) + { + WriteMapped8(MEM_AHMFLUSH_ACK, ReadMapped8(MEM_AHMFLUSH)); + WriteMapped8(MEM_AHMFLUSH_ACK + 1, ReadMapped8(MEM_AHMFLUSH + 1)); + } + if ((address & 3) != 3) + return; + + const u32 word = ReadRegister(word_address); + if (word_address == HW_USBFRCRST && (word & ~m_usb_force_reset_before_write) != 0) + { + // IOS asserts Hollywood's USB force-reset lines as a group before loading + // the host-controller modules, then releases them one by one. The three + // controllers share this reset domain: keeping their previous operational + // registers across an IOS reload makes the next OHCI driver reject the + // hardware before it can issue HCR itself. + ResetEHCIController(); + for (size_t controller = 0; controller < OHCI_BASES.size(); ++controller) + ResetOHCIController(controller); + DEBUG_LOG_FMT(IOS, "Starlet USB force reset asserted: {:#010x} -> {:#010x}", + m_usb_force_reset_before_write, word); + } + if (word_address == HW_USBFRCRST || word_address == HW_IFPOWER || word_address == HW_IOPWRCTRL) + { + const Starlet* const starlet = m_system.GetStarlet(); + DEBUG_LOG_FMT(IOS, "Starlet power register write address={:#010x} value={:#010x} pc={:#010x}", + word_address, word, starlet ? starlet->GetPC() : 0); + } + if (word_address == NAND_CTRL) + HandleNANDCommand(word); + else if (word_address >= AES_BASE && word_address <= AES_IV) + HandleAESWrite(word_address, word); + else if (word_address >= SHA_BASE && word_address <= SHA_H4) + HandleSHAWrite(word_address, word); + else if (word_address == HW_OTPCMD) + HandleOTPCommand(word); + else if (word_address == HW_TIMER) + m_arm_cycles = static_cast(word) * 405 / 32; + else if (word_address == HW_ALARM) + { + // HW_ALARM is a comparator, not an interrupt acknowledgement register. + // Original Starlet software acknowledges the timer separately through + // HW_ARMIRQFLAG. Programming the comparator to the current counter value + // creates an immediate match. + if (word == GetTimer()) + m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_TIMER, true); + } + else if (word_address == HW_SPARE0) + WriteRegister(HW_BOOT0, (word & (1U << 16)) != 0 ? 0 : 9); + else if (word_address == HW_BOOT0) + m_boot0_mapped = (word & BOOT0_DISABLE) == 0; + else if (word_address == HW_SRNPROT) + m_sram_split_mode = (word & SRNPROT_SRAM_SPLIT_MODE) != 0; + else if (word_address == HW_GPIO_OUT) + HandleGPIOWrite(word); + else if (ehci) + HandleEHCIWrite(word_address); + else if (ohci_controller) + HandleOHCIWrite(*ohci_controller, word_address); + else if (word_address >= SDHC_BASE && word_address < SDHC_BASE + SDHC_SIZE) + HandleSDHCWrite(word_address); + else if (wifi_sdio) + HandleWiFiSDIOWrite(word_address); + else if ((word_address >= HW_BASE && word_address <= HW_BASE + 0x0c) || + (word_address >= HW_BASE + 0x30 && word_address <= HW_BASE + 0x40) || + word_address == HW_AHBPROT || word_address == HW_RESETS) + { + m_system.GetWiiIPC().WriteStarletRegister(word_address - HW_BASE, word); + if (word_address == HW_BASE + 0x0c && (word & 0x09) != 0) + { + // The physical CPUs are concurrent. Give Broadway a scheduling boundary + // when Starlet exposes an acknowledgement or reply. + if (Starlet* const starlet = m_system.GetStarlet()) + starlet->YieldForIPC(); + } + } +} + +void StarletMemory::Write16(u32 address, u16 value) +{ + if (IsMemoryAddress(address) && IsMemoryAddress(address + 1)) + { + m_system.GetMemory().Write_U16(value, address); + return; + } + + ARMBus::Write16(address, value); +} + +void StarletMemory::Write32(u32 address, u32 value) +{ + if ((address & 3) == 0 && IsDIAddress(address)) + { + MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping(); + if (mmio) + mmio->Write(m_system, address, value); + return; + } + + if (IsMemoryAddress(address) && IsMemoryAddress(address + 3)) + { + m_system.GetMemory().Write_U32(value, address); + return; + } + + ARMBus::Write32(address, value); +} + +void StarletMemory::HandleNANDCommand(u32 command) +{ + // IOS and mini acknowledge the NAND block by writing all non-EXEC bits as + // ones. This deasserts the block interrupt but does not turn those ones into + // the next readable command/status value. + if (command == NAND_CTRL_ACK) + { + WriteRegister(NAND_CTRL, m_nand_control_before_write); + m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_NAND, false); + return; + } + + if (!(command & NAND_CTRL_EXEC)) + return; + + if (m_nand_cycles_until_completion != 0) + { + WARN_LOG_FMT(IOS, + "Dropping Starlet NAND command {:#010x} at PC {:#010x}; " + "command {:#010x} still has {:#x} cycles", + command, m_system.GetStarlet() ? m_system.GetStarlet()->GetPC() : 0, + m_nand_pending_command, m_nand_cycles_until_completion); + return; + } + + // The Samsung K9F4G08U0A keeps R/B busy while the array operation completes. + // IOS waits for the controller interrupt, so completing commands in this MMIO + // write would keep the high-priority FS thread permanently runnable and + // starve every lower-priority IOS module. + constexpr u64 arm_cycles_per_microsecond = 243; + const u8 nand_command = static_cast(command >> 16); + switch (nand_command) + { + case NAND_CMD_READ_CONFIRM: + case NAND_CMD_RANDOM_OUTPUT_CONFIRM: + m_nand_cycles_until_completion = 25 * arm_cycles_per_microsecond; + break; + case NAND_CMD_PROGRAM_CONFIRM: + m_nand_cycles_until_completion = 200 * arm_cycles_per_microsecond; + break; + case NAND_CMD_ERASE_CONFIRM: + m_nand_cycles_until_completion = 1500 * arm_cycles_per_microsecond; + break; + default: + // Setup, address, data-register, ID, status and reset commands do not + // perform an array operation. In particular, IOS writes the setup and + // confirm phases back-to-back. + CompleteNANDCommand(command); + return; + } + m_nand_pending_command = command; + m_nand_status &= ~NAND_STATUS_READY; + WriteRegister(NAND_CTRL, command); +} + +void StarletMemory::CompleteNANDCommand(u32 command) +{ + const u8 nand_command = static_cast(command >> 16); + bool succeeded = true; + switch (nand_command) + { + case NAND_CMD_RESET: + ResetNANDOperationState(); + break; + case NAND_CMD_READ_ID: + succeeded = ReadNANDID(command); + break; + case NAND_CMD_STATUS: + case NAND_CMD_STATUS_MULTI: + succeeded = ReadNANDStatus(command); + break; + case NAND_CMD_READ_PRE: + m_nand_read_page = ReadRegister(NAND_ADDR2) & 0xffffff; + m_nand_read_column = ReadRegister(NAND_ADDR1) & 0xffff; + m_nand_read_pending = + m_nand_read_page < NAND_PAGE_COUNT && m_nand_read_column < NAND_RAW_PAGE_SIZE; + succeeded = m_nand_read_pending; + break; + case NAND_CMD_READ_SPARE: + m_nand_read_page = ReadRegister(NAND_ADDR2) & 0xffffff; + m_nand_read_column = NAND_PAGE_DATA_SIZE + (ReadRegister(NAND_ADDR1) & 0x3f); + m_nand_read_pending = + m_nand_read_page < NAND_PAGE_COUNT && m_nand_read_column < NAND_RAW_PAGE_SIZE; + succeeded = m_nand_read_pending; + break; + case NAND_CMD_RANDOM_OUTPUT: + if (m_nand_read_pending) + m_nand_read_column = ReadRegister(NAND_ADDR1) & 0xffff; + succeeded = m_nand_read_pending && m_nand_read_column < NAND_RAW_PAGE_SIZE; + break; + case NAND_CMD_READ_CONFIRM: + case NAND_CMD_RANDOM_OUTPUT_CONFIRM: + succeeded = ReadNANDPage(command); + break; + case NAND_CMD_PROGRAM_PRE: + succeeded = StageNANDProgram(command, false); + break; + case NAND_CMD_RANDOM_INPUT: + succeeded = StageNANDProgram(command, true); + break; + case NAND_CMD_PROGRAM_CONFIRM: + succeeded = CommitNANDProgram(); + break; + case NAND_CMD_ERASE_PRE: + succeeded = StageNANDErase(); + break; + case NAND_CMD_ERASE_CONFIRM: + succeeded = CommitNANDErase(); + break; + default: + succeeded = false; + WARN_LOG_FMT(IOS, "Unsupported Starlet NAND command {:#04x}", nand_command); + break; + } + + if (!succeeded) + { + command |= NAND_CTRL_ERROR; + m_nand_status |= NAND_STATUS_FAIL; + } + + m_nand_status |= NAND_STATUS_READY; + + WriteRegister(NAND_CTRL, command & ~NAND_CTRL_EXEC); + // Commands used for synchronous setup are polled through EXEC. Only commands + // carrying the IRQ flag may signal Hollywood; otherwise READ_PRE would + // satisfy FS's wait before READ_CONFIRM has transferred the page. + if ((command & NAND_CTRL_IRQ) != 0) + m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_NAND, true); +} + +bool StarletMemory::ReadRawNANDPage(u32 page, NANDPage* raw) +{ + if (page >= NAND_PAGE_COUNT) + return false; + + const auto overlay = m_nand_overlay.find(page); + if (overlay != m_nand_overlay.end()) + { + *raw = overlay->second; + return true; + } + + m_nand.ClearError(); + return m_nand.Seek(static_cast(page) * NAND_RAW_PAGE_SIZE, File::SeekOrigin::Begin) && + m_nand.ReadArray(raw); +} + +bool StarletMemory::InstallEmulatedWiimotePairings() +{ + using NANDSuperblock = DiscIO::NANDImporter::NANDSuperblock; + using NANDFSTEntry = DiscIO::NANDImporter::NANDFSTEntry; + + constexpr u32 NAND_SUPERBLOCK_START = 0x1fc00000; + constexpr u32 NAND_SUPERBLOCK_COUNT = 16; + constexpr u32 NAND_CLUSTER_SIZE = 0x4000; + constexpr u32 NAND_PAGES_PER_CLUSTER = NAND_CLUSTER_SIZE / NAND_PAGE_DATA_SIZE; + constexpr u16 NAND_CLUSTER_CHAIN_END = 0xfffb; + constexpr u16 NAND_INVALID_ENTRY = 0xffff; + + const auto read_logical_data = [this](u32 logical_offset, u8* destination, size_t size) { + if ((logical_offset % NAND_PAGE_DATA_SIZE) != 0) + return false; + + size_t copied = 0; + while (copied < size) + { + NANDPage raw{}; + const u32 page = + logical_offset / NAND_PAGE_DATA_SIZE + static_cast(copied / NAND_PAGE_DATA_SIZE); + if (!ReadRawNANDPage(page, &raw)) + return false; + const size_t amount = std::min(NAND_PAGE_DATA_SIZE, size - copied); + std::copy_n(raw.begin(), amount, destination + copied); + copied += amount; + } + return true; + }; + + // SFFS superblocks are stored without NAND data encryption. Select the newest + // valid copy exactly as Dolphin's BootMii importer does, but read only the + // few pages needed from the source dump. + std::unique_ptr superblock; + for (u32 index = 0; index < NAND_SUPERBLOCK_COUNT; ++index) + { + auto candidate = std::make_unique(); + if (!read_logical_data(NAND_SUPERBLOCK_START + index * sizeof(NANDSuperblock), + reinterpret_cast(candidate.get()), sizeof(NANDSuperblock))) + return false; + if (!std::ranges::equal(candidate->magic, std::array{'S', 'F', 'F', 'S'})) + continue; + if (!superblock || candidate->version > superblock->version) + superblock = std::move(candidate); + } + if (!superblock) + { + ERROR_LOG_FMT(IOS_WIIMOTE, "Raw NAND has no valid SFFS superblock"); + return false; + } + + const auto entry_name = [](const NANDFSTEntry& entry) { + const char* const end = std::find(entry.name, std::end(entry.name), '\0'); + return std::string_view(entry.name, static_cast(end - entry.name)); + }; + const auto find_child = [&superblock, &entry_name](u16 parent, std::string_view name) { + if (parent >= superblock->fst.size()) + return std::optional{}; + u16 child = superblock->fst[parent].sub; + while (child != NAND_INVALID_ENTRY) + { + if (child >= superblock->fst.size()) + return std::optional{}; + if (entry_name(superblock->fst[child]) == name) + return std::optional{child}; + child = superblock->fst[child].sib; + } + return std::optional{}; + }; + + std::optional entry = 0; + for (const std::string_view component : {"shared2", "sys", "SYSCONF"}) + { + entry = entry ? find_child(*entry, component) : std::nullopt; + if (!entry) + { + ERROR_LOG_FMT(IOS_WIIMOTE, "Raw NAND SFFS is missing /shared2/sys/SYSCONF"); + return false; + } + } + + const NANDFSTEntry& sysconf_entry = superblock->fst[*entry]; + if ((sysconf_entry.mode & 3) != static_cast(DiscIO::NANDImporter::Type::File) || + sysconf_entry.size == 0) + return false; + + auto decrypt = Common::AES::CreateContextDecrypt(m_keys.data() + NAND_AES_KEY_OFFSET_IN_KEYS); + auto encrypt = Common::AES::CreateContextEncrypt(m_keys.data() + NAND_AES_KEY_OFFSET_IN_KEYS); + if (!decrypt || !encrypt) + return false; + + std::vector sysconf; + sysconf.reserve(sysconf_entry.size); + std::vector cluster_indices; + std::vector> decrypted_clusters; + size_t remaining = sysconf_entry.size; + u16 cluster = sysconf_entry.sub; + while (remaining != 0) + { + if (cluster >= superblock->fat.size() || cluster == NAND_CLUSTER_CHAIN_END) + return false; + + std::array encrypted_cluster{}; + std::array decrypted_cluster{}; + if (!read_logical_data(static_cast(cluster) * NAND_CLUSTER_SIZE, encrypted_cluster.data(), + encrypted_cluster.size()) || + !decrypt->CryptIvZero(encrypted_cluster.data(), decrypted_cluster.data(), + decrypted_cluster.size())) + return false; + + const size_t amount = std::min(remaining, decrypted_cluster.size()); + sysconf.insert(sysconf.end(), decrypted_cluster.begin(), decrypted_cluster.begin() + amount); + cluster_indices.push_back(cluster); + decrypted_clusters.push_back(std::move(decrypted_cluster)); + remaining -= amount; + cluster = superblock->fat[cluster]; + } + + const auto read_be16 = [&sysconf](size_t offset) -> std::optional { + if (offset + 2 > sysconf.size()) + return std::nullopt; + return static_cast((static_cast(sysconf[offset]) << 8) | sysconf[offset + 1]); + }; + if (sysconf.size() < 8 || + !std::ranges::equal(std::array{'S', 'C', 'v', '0'}, std::span(sysconf).first<4>())) + return false; + + const std::optional entry_count = read_be16(4); + if (!entry_count || 6 + (static_cast(*entry_count) + 1) * 2 > sysconf.size()) + return false; + + std::optional pairing_offset; + std::optional pairing_size; + for (u16 index = 0; index < *entry_count; ++index) + { + const std::optional raw_offset = read_be16(6 + index * 2); + if (!raw_offset || *raw_offset >= sysconf.size()) + return false; + const u8 description = sysconf[*raw_offset]; + const u8 type = description >> 5; + const size_t name_size = (description & 0x1f) + 1; + const size_t value_header = static_cast(*raw_offset) + 1 + name_size; + if (value_header + 2 > sysconf.size()) + return false; + const std::string_view name(reinterpret_cast(sysconf.data() + *raw_offset + 1), + name_size); + if (name == "BT.DINF" && type == 1) + { + const std::optional size_minus_one = read_be16(value_header); + if (!size_minus_one) + return false; + pairing_offset = value_header + 2; + pairing_size = static_cast(*size_minus_one) + 1; + break; + } + } + + const std::vector pairing = IOS::HLE::GetEmulatedWiimotePairingData(); + if (!pairing_offset || !pairing_size || *pairing_size != pairing.size() || + *pairing_offset + *pairing_size > sysconf.size()) + { + ERROR_LOG_FMT(IOS_WIIMOTE, "Raw NAND SYSCONF has no compatible BT.DINF section"); + return false; + } + + // Prefer identities that the dumped System Menu already trusts. BT.DINF + // stores addresses in reverse byte order; never log them, since they came + // from the user's console. Active devices define the preferred order, + // followed by any remaining registered devices. + constexpr size_t CONF_PAD_DEVICE_SIZE = 6 + 0x40; + constexpr size_t CONF_PAD_REGISTERED_COUNT = 10; + constexpr size_t CONF_PAD_REGISTERED_OFFSET = 1; + constexpr size_t CONF_PAD_ACTIVE_OFFSET = + CONF_PAD_REGISTERED_OFFSET + CONF_PAD_REGISTERED_COUNT * CONF_PAD_DEVICE_SIZE; + size_t trusted_count = 0; + const auto add_trusted_address = [&](size_t device_offset) { + if (trusted_count >= m_wiimote_addresses.size() || + device_offset + 6 > *pairing_offset + *pairing_size) + return; + bdaddr_t address{}; + std::reverse_copy(sysconf.begin() + static_cast(device_offset), + sysconf.begin() + static_cast(device_offset + 6), address.begin()); + if (std::ranges::all_of(address, [](u8 byte) { return byte == 0 || byte == 0xff; }) || + std::ranges::find(std::span(m_wiimote_addresses).first(trusted_count), address) != + std::span(m_wiimote_addresses).first(trusted_count).end()) + return; + m_wiimote_addresses[trusted_count++] = address; + }; + for (size_t index = 0; index < m_wiimote_addresses.size(); ++index) + add_trusted_address(*pairing_offset + CONF_PAD_ACTIVE_OFFSET + index * CONF_PAD_DEVICE_SIZE); + const size_t registered_count = + std::min(sysconf[*pairing_offset], CONF_PAD_REGISTERED_COUNT); + for (size_t index = 0; index < registered_count; ++index) + add_trusted_address(*pairing_offset + CONF_PAD_REGISTERED_OFFSET + + index * CONF_PAD_DEVICE_SIZE); + + if (trusted_count != 0) + { + for (size_t index = trusted_count; index < m_wiimote_addresses.size(); ++index) + m_wiimote_addresses[index] = {0x11, 0x02, 0x19, 0x79, 0, static_cast(index)}; + INFO_LOG_FMT(IOS_WIIMOTE, + "Configured {} emulated Wii Remote identity/identities from " + "raw NAND pairing data", + trusted_count); + return true; + } + + std::ranges::copy(pairing, sysconf.begin() + static_cast(*pairing_offset)); + + // Re-encrypt only the file's SFFS clusters and place their raw pages in the + // session overlay. Preserve all spare metadata from the dump and replace only + // the ECC for the modified ciphertext. + size_t file_offset = 0; + for (size_t cluster_number = 0; cluster_number < cluster_indices.size(); ++cluster_number) + { + auto& plaintext = decrypted_clusters[cluster_number]; + const size_t amount = std::min(sysconf.size() - file_offset, plaintext.size()); + std::copy_n(sysconf.begin() + static_cast(file_offset), amount, plaintext.begin()); + file_offset += amount; + + std::array encrypted_cluster{}; + if (!encrypt->CryptIvZero(plaintext.data(), encrypted_cluster.data(), encrypted_cluster.size())) + return false; + + // IOS authenticates the 0x40-byte file salt followed by the plaintext + // cluster. The salt's integer fields are big-endian because it is + // assembled by the big-endian Starlet CPU. + std::array salt{}; + const auto write_be32 = [&salt](size_t offset, u32 value) { + salt[offset] = static_cast(value >> 24); + salt[offset + 1] = static_cast(value >> 16); + salt[offset + 2] = static_cast(value >> 8); + salt[offset + 3] = static_cast(value); + }; + write_be32(0x00, sysconf_entry.uid); + std::copy(std::begin(sysconf_entry.name), std::end(sysconf_entry.name), salt.begin() + 0x04); + write_be32(0x10, static_cast(cluster_number)); + write_be32(0x14, *entry); + write_be32(0x18, sysconf_entry.x3); + + std::vector authenticated_data; + authenticated_data.reserve(salt.size() + plaintext.size()); + authenticated_data.insert(authenticated_data.end(), salt.begin(), salt.end()); + authenticated_data.insert(authenticated_data.end(), plaintext.begin(), plaintext.end()); + std::array hmac{}; + if (!Common::HMAC::HMACWithSHA1( + std::span(m_keys).subspan(NAND_HMAC_KEY_OFFSET_IN_KEYS, hmac.size()), + authenticated_data, hmac.data())) + return false; + + const u32 first_page = + static_cast(cluster_indices[cluster_number]) * NAND_PAGES_PER_CLUSTER; + for (u32 page_in_cluster = 0; page_in_cluster < NAND_PAGES_PER_CLUSTER; ++page_in_cluster) + { + NANDPage raw{}; + const u32 page = first_page + page_in_cluster; + if (!ReadRawNANDPage(page, &raw)) + return false; + std::copy_n(encrypted_cluster.begin() + page_in_cluster * NAND_PAGE_DATA_SIZE, + NAND_PAGE_DATA_SIZE, raw.begin()); + // SFFS keeps redundant HMACs at spare offset 0x0c in the seventh and + // eighth pages. + if (page_in_cluster >= 6) + std::ranges::copy(hmac, raw.begin() + NAND_PAGE_DATA_SIZE + 0x0c); + const auto ecc = CalculateNANDECC(raw.data()); + std::ranges::copy(ecc, raw.begin() + NAND_PAGE_DATA_SIZE + 0x30); + m_nand_overlay.insert_or_assign(page, raw); + } + } + + INFO_LOG_FMT(IOS_WIIMOTE, "Installed emulated Wii Remote pairings in the raw " + "NAND copy-on-write overlay"); + return true; +} + +bool StarletMemory::ReadNANDPage(u32 command) +{ + if (!(command & NAND_CTRL_READ) || !m_nand_read_pending) + return false; + + const u32 length = command & NAND_CTRL_DATA_LENGTH_MASK; + if (length == 0 || m_nand_read_column + length > NAND_RAW_PAGE_SIZE) + return false; + + const u32 data_address = ReadRegister(NAND_DATA) & ~0xfU; + const u32 spare_address = ReadRegister(NAND_ECC) & ~0xfU; + NANDPage raw{}; + if (!ReadRawNANDPage(m_nand_read_page, &raw)) + return false; + + for (u32 i = 0; i < length; ++i) + { + const u32 raw_offset = m_nand_read_column + i; + u32 destination = data_address + i; + if (m_nand_read_column < NAND_PAGE_DATA_SIZE && raw_offset >= NAND_PAGE_DATA_SIZE) + destination = spare_address + raw_offset - NAND_PAGE_DATA_SIZE; + WriteMapped8(destination, raw[raw_offset]); + } + + if (command & NAND_CTRL_ECC) + { + const auto ecc = CalculateNANDECC(raw.data()); + const u32 calculated_ecc_address = spare_address ^ 0x40U; + for (u32 i = 0; i < ecc.size(); ++i) + WriteMapped8(calculated_ecc_address + i, ecc[i]); + } + return true; +} + +bool StarletMemory::ReadNANDID(u32 command) +{ + if (!(command & NAND_CTRL_READ)) + return false; + + const u32 length = command & NAND_CTRL_DATA_LENGTH_MASK; + if (length == 0) + return false; + + const u32 data_address = ReadRegister(NAND_DATA) & ~0xfU; + for (u32 i = 0; i < length; ++i) + WriteMapped8(data_address + i, NAND_CHIP_ID[i % NAND_CHIP_ID.size()]); + return true; +} + +bool StarletMemory::ReadNANDStatus(u32 command) +{ + if (!(command & NAND_CTRL_READ)) + return false; + + const u32 length = command & NAND_CTRL_DATA_LENGTH_MASK; + if (length == 0) + return false; + + const u32 data_address = ReadRegister(NAND_DATA) & ~0xfU; + for (u32 i = 0; i < length; ++i) + WriteMapped8(data_address + i, m_nand_status); + return true; +} + +bool StarletMemory::StageNANDProgram(u32 command, bool random_data_input) +{ + if (!(command & NAND_CTRL_WRITE)) + return false; + + const u32 length = command & NAND_CTRL_DATA_LENGTH_MASK; + const u32 column = ReadRegister(NAND_ADDR1) & 0xffff; + if (length == 0 || column + length > NAND_RAW_PAGE_SIZE) + return false; + + if (random_data_input) + { + if (!m_nand_program_pending) + return false; + } + else + { + m_nand_program_page = ReadRegister(NAND_ADDR2) & 0xffffff; + if (m_nand_program_page >= NAND_PAGE_COUNT) + return false; + m_nand_program_data.fill(0xff); + m_nand_program_pending = true; + m_nand_program_ecc_enabled = false; + m_nand_status &= ~NAND_STATUS_FAIL; + } + + const u32 data_address = ReadRegister(NAND_DATA) & ~0xfU; + const u32 spare_address = ReadRegister(NAND_ECC) & ~0xfU; + for (u32 i = 0; i < length; ++i) + { + const u32 raw_offset = column + i; + u32 source = data_address + i; + if (column < NAND_PAGE_DATA_SIZE && raw_offset >= NAND_PAGE_DATA_SIZE) + source = spare_address + raw_offset - NAND_PAGE_DATA_SIZE; + m_nand_program_data[raw_offset] = ReadMapped8(source); + } + + if ((command & NAND_CTRL_ECC) != 0 && column == 0 && length >= NAND_PAGE_DATA_SIZE) + { + const auto ecc = CalculateNANDECC(m_nand_program_data.data()); + const u32 calculated_ecc_address = spare_address ^ 0x40U; + for (u32 i = 0; i < ecc.size(); ++i) + WriteMapped8(calculated_ecc_address + i, ecc[i]); + m_nand_program_ecc_enabled = true; + } + return true; +} + +bool StarletMemory::CommitNANDProgram() +{ + if (!m_nand_program_pending) + return false; + + NANDPage raw{}; + const bool read_succeeded = ReadRawNANDPage(m_nand_program_page, &raw); + if (read_succeeded) + { + // ECC-enabled page programming calculates the syndrome from the page data + // and places it in the final 16 bytes of the NAND spare area. IOS supplies + // the other spare bytes separately with RANDOM DATA INPUT. + if (m_nand_program_ecc_enabled) + { + const auto ecc = CalculateNANDECC(m_nand_program_data.data()); + std::copy(ecc.begin(), ecc.end(), m_nand_program_data.begin() + NAND_PAGE_DATA_SIZE + 0x30); + } + + // NAND programming can only change a bit from 1 to 0. An erase is required + // to restore it. + for (u32 i = 0; i < NAND_RAW_PAGE_SIZE; ++i) + raw[i] &= m_nand_program_data[i]; + m_nand_overlay.insert_or_assign(m_nand_program_page, raw); + m_nand_status &= ~NAND_STATUS_FAIL; + } + else + { + m_nand_status |= NAND_STATUS_FAIL; + } + + m_nand_program_pending = false; + m_nand_program_ecc_enabled = false; + return read_succeeded; +} + +bool StarletMemory::StageNANDErase() +{ + m_nand_erase_page = ReadRegister(NAND_ADDR2) & 0xffffff; + m_nand_erase_pending = m_nand_erase_page < NAND_PAGE_COUNT; + if (m_nand_erase_pending) + m_nand_status &= ~NAND_STATUS_FAIL; + return m_nand_erase_pending; +} + +bool StarletMemory::CommitNANDErase() +{ + if (!m_nand_erase_pending) + return false; + + const u32 block_start = m_nand_erase_page & ~(NAND_PAGES_PER_BLOCK - 1); + NANDPage erased_page{}; + erased_page.fill(0xff); + for (u32 page = block_start; page < block_start + NAND_PAGES_PER_BLOCK; ++page) + m_nand_overlay.insert_or_assign(page, erased_page); + + m_nand_erase_pending = false; + m_nand_status &= ~NAND_STATUS_FAIL; + return true; +} + +void StarletMemory::ResetNANDOperationState() +{ + m_nand_program_data.fill(0xff); + m_nand_read_page = 0; + m_nand_read_column = 0; + m_nand_program_page = 0; + m_nand_erase_page = 0; + m_nand_status = NAND_STATUS_READY | NAND_STATUS_NOT_WRITE_PROTECTED; + m_nand_read_pending = false; + m_nand_program_pending = false; + m_nand_program_ecc_enabled = false; + m_nand_erase_pending = false; + m_nand_pending_command = 0; + m_nand_cycles_until_completion = 0; +} + +bool StarletMemory::IsParityOdd(u8 value) +{ + bool parity = false; + while (value != 0) + { + parity ^= (value & 1) != 0; + value >>= 1; + } + return parity; +} + +std::array StarletMemory::CalculateNANDECC(const u8* data) +{ + std::array result{}; + for (u32 sector = 0; sector < 4; ++sector) + { + u8 accumulators[12][2]{}; + for (u32 i = 0; i < 512; ++i) + { + const u8 byte = data[sector * 512 + i]; + for (u32 bit = 0; bit < 9; ++bit) + accumulators[3 + bit][(i >> bit) & 1] ^= byte; + } + + const u8 combined = accumulators[3][0] ^ accumulators[3][1]; + accumulators[0][0] = combined & 0x55; + accumulators[0][1] = combined & 0xaa; + accumulators[1][0] = combined & 0x33; + accumulators[1][1] = combined & 0xcc; + accumulators[2][0] = combined & 0x0f; + accumulators[2][1] = combined & 0xf0; + + u32 even = 0; + u32 odd = 0; + for (u32 bit = 0; bit < 12; ++bit) + { + even |= static_cast(IsParityOdd(accumulators[bit][0])) << bit; + odd |= static_cast(IsParityOdd(accumulators[bit][1])) << bit; + } + result[sector * 4] = static_cast(even); + result[sector * 4 + 1] = static_cast(even >> 8); + result[sector * 4 + 2] = static_cast(odd); + result[sector * 4 + 3] = static_cast(odd >> 8); + } + return result; +} + +void StarletMemory::HandleAESWrite(u32 address, u32 value) +{ + if (address == AES_KEY) + { + std::move(m_aes_key.begin() + 4, m_aes_key.end(), m_aes_key.begin()); + for (u32 i = 0; i < 4; ++i) + m_aes_key[12 + i] = static_cast(value >> (24 - i * 8)); + } + else if (address == AES_IV) + { + std::move(m_aes_iv.begin() + 4, m_aes_iv.end(), m_aes_iv.begin()); + for (u32 i = 0; i < 4; ++i) + m_aes_iv[12 + i] = static_cast(value >> (24 - i * 8)); + } + else if (address == AES_CMD) + { + ExecuteAESCommand(value); + } +} + +void StarletMemory::ExecuteAESCommand(u32 command) +{ + if (command == 0) + return; + if ((command & AES_CTRL_EXEC) == 0) + return; + + const size_t size = (static_cast(command & 0xfff) + 1) * 16; + const u32 source = ReadRegister(AES_SRC) & ~0xfU; + const u32 destination = ReadRegister(AES_DEST) & ~0xfU; + std::vector input(size); + std::vector output(size); + for (size_t i = 0; i < size; ++i) + input[i] = ReadMapped8(source + static_cast(i)); + + std::array next_iv{}; + bool succeeded = true; + if ((command & AES_CTRL_ENABLE) != 0) + { + auto context = (command & AES_CTRL_DECRYPT) != 0 ? + Common::AES::CreateContextDecrypt(m_aes_key.data()) : + Common::AES::CreateContextEncrypt(m_aes_key.data()); + succeeded = context->Crypt(m_aes_iv.data(), next_iv.data(), input.data(), output.data(), size); + } + else + { + output = input; + } + + if (succeeded) + { + if ((command & AES_CTRL_ENABLE) != 0) + m_aes_iv = next_iv; + for (size_t i = 0; i < size; ++i) + WriteMapped8(destination + static_cast(i), output[i]); + WriteRegister(AES_SRC, source + static_cast(size)); + WriteRegister(AES_DEST, destination + static_cast(size)); + } + WriteRegister(AES_CMD, command & ~AES_CTRL_EXEC); + if ((command & AES_CTRL_IRQ) != 0) + m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_AES, true); +} + +void StarletMemory::HandleSHAWrite(u32 address, u32 value) +{ + if (address >= SHA_H0) + m_sha_state[(address - SHA_H0) / 4] = value; + else if (address == SHA_CMD) + ExecuteSHACommand(value); +} + +u32 StarletMemory::RotateLeft(u32 value, u32 amount) +{ + return (value << amount) | (value >> (32 - amount)); +} + +void StarletMemory::CompressSHA1(const u8* block) +{ + std::array words{}; + for (u32 i = 0; i < 16; ++i) + { + words[i] = (static_cast(block[i * 4]) << 24) | (static_cast(block[i * 4 + 1]) << 16) | + (static_cast(block[i * 4 + 2]) << 8) | block[i * 4 + 3]; + } + for (u32 i = 16; i < 80; ++i) + words[i] = RotateLeft(words[i - 3] ^ words[i - 8] ^ words[i - 14] ^ words[i - 16], 1); + + u32 a = m_sha_state[0]; + u32 b = m_sha_state[1]; + u32 c = m_sha_state[2]; + u32 d = m_sha_state[3]; + u32 e = m_sha_state[4]; + for (u32 i = 0; i < 80; ++i) + { + u32 function; + u32 constant; + if (i < 20) + { + function = (b & c) | (~b & d); + constant = 0x5a827999; + } + else if (i < 40) + { + function = b ^ c ^ d; + constant = 0x6ed9eba1; + } + else if (i < 60) + { + function = (b & c) | (b & d) | (c & d); + constant = 0x8f1bbcdc; + } + else + { + function = b ^ c ^ d; + constant = 0xca62c1d6; + } + const u32 temporary = RotateLeft(a, 5) + function + e + constant + words[i]; + e = d; + d = c; + c = RotateLeft(b, 30); + b = a; + a = temporary; + } + m_sha_state[0] += a; + m_sha_state[1] += b; + m_sha_state[2] += c; + m_sha_state[3] += d; + m_sha_state[4] += e; +} + +void StarletMemory::ExecuteSHACommand(u32 command) +{ + if (!(command & (1U << 31))) + return; + + const u32 source = ReadRegister(SHA_SRC) & ~0x3fU; + const u32 blocks = (command & 0x3ff) + 1; + std::array block{}; + for (u32 i = 0; i < blocks; ++i) + { + for (u32 j = 0; j < block.size(); ++j) + block[j] = ReadMapped8(source + i * 64 + j); + CompressSHA1(block.data()); + } + for (u32 i = 0; i < m_sha_state.size(); ++i) + WriteRegister(SHA_H0 + i * 4, m_sha_state[i]); + WriteRegister(SHA_SRC, source + blocks * 64); + WriteRegister(SHA_CMD, command & ~(1U << 31)); + m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_SHA1, true); +} + +void StarletMemory::HandleOTPCommand(u32 command) +{ + if (!(command & (1U << 31))) + return; + const size_t offset = BOOT1_HASH_OFFSET_IN_KEYS + (command & 0x1f) * 4; + if (offset + 4 > m_keys.size()) + return; + const u32 value = (static_cast(m_keys[offset]) << 24) | + (static_cast(m_keys[offset + 1]) << 16) | + (static_cast(m_keys[offset + 2]) << 8) | m_keys[offset + 3]; + WriteRegister(HW_OTPDATA, value); +} + +void StarletMemory::HandleGPIOWrite(u32 value) +{ + const bool old_cs = (m_gpio_out & GPIO_EEP_CS) != 0; + const bool old_clock = (m_gpio_out & GPIO_EEP_CLK) != 0; + const bool chip_selected = (value & GPIO_EEP_CS) != 0; + const bool clock = (value & GPIO_EEP_CLK) != 0; + m_gpio_out = value; + + if (!chip_selected) + { + m_seeprom_command = 0; + m_seeprom_command_bits = 0; + m_seeprom_output_bits = 0; + m_seeprom_input = 0; + m_seeprom_input_bits = 0; + m_seeprom_write_pending = false; + m_seeprom_write_all = false; + m_seeprom_miso = false; + return; + } + if (!old_cs) + { + m_seeprom_command = 0; + m_seeprom_command_bits = 0; + m_seeprom_output_bits = 0; + m_seeprom_input = 0; + m_seeprom_input_bits = 0; + m_seeprom_write_pending = false; + m_seeprom_write_all = false; + m_seeprom_miso = false; + } + if (old_clock || !clock) + return; + + if (m_seeprom_command_bits < 11) + { + m_seeprom_command = + static_cast((m_seeprom_command << 1) | ((value & GPIO_EEP_MOSI) != 0 ? 1 : 0)); + ++m_seeprom_command_bits; + if (m_seeprom_command_bits == 11) + { + const u16 opcode = m_seeprom_command & 0x700; + const u8 word = static_cast(m_seeprom_command); + const size_t offset = static_cast(word) * 2; + if (opcode == 0x600 && word < 128) + { + m_seeprom_output = + static_cast((static_cast(m_seeprom[offset]) << 8) | m_seeprom[offset + 1]); + m_seeprom_output_bits = 16; + } + else if (opcode == 0x500 && m_seeprom_write_enabled && word < 128) + { + m_seeprom_write_address = word; + m_seeprom_write_pending = true; + } + else if (opcode == 0x700 && m_seeprom_write_enabled && word < 128) + { + m_seeprom[offset] = 0xff; + m_seeprom[offset + 1] = 0xff; + } + else if ((m_seeprom_command & 0x7c0) == 0x4c0) + { + m_seeprom_write_enabled = true; + } + else if ((m_seeprom_command & 0x7c0) == 0x400) + { + m_seeprom_write_enabled = false; + } + else if ((m_seeprom_command & 0x7c0) == 0x480 && m_seeprom_write_enabled) + { + m_seeprom.fill(0xff); + } + else if ((m_seeprom_command & 0x7c0) == 0x440 && m_seeprom_write_enabled) + { + m_seeprom_write_pending = true; + m_seeprom_write_all = true; + } + } + return; + } + + if (m_seeprom_output_bits != 0) + { + --m_seeprom_output_bits; + m_seeprom_miso = ((m_seeprom_output >> m_seeprom_output_bits) & 1) != 0; + } + else if (m_seeprom_write_pending) + { + m_seeprom_input = + static_cast((m_seeprom_input << 1) | ((value & GPIO_EEP_MOSI) != 0 ? 1 : 0)); + ++m_seeprom_input_bits; + if (m_seeprom_input_bits == 16) + { + if (m_seeprom_write_all) + { + for (size_t offset = 0; offset < m_seeprom.size(); offset += 2) + { + m_seeprom[offset] = static_cast(m_seeprom_input >> 8); + m_seeprom[offset + 1] = static_cast(m_seeprom_input); + } + } + else + { + const size_t offset = static_cast(m_seeprom_write_address) * 2; + m_seeprom[offset] = static_cast(m_seeprom_input >> 8); + m_seeprom[offset + 1] = static_cast(m_seeprom_input); + } + m_seeprom_write_pending = false; + } + } +} +} // namespace IOS::LLE diff --git a/Source/Core/Core/IOS/Starlet/StarletMemory.h b/Source/Core/Core/IOS/Starlet/StarletMemory.h new file mode 100644 index 0000000000..7cecba7cae --- /dev/null +++ b/Source/Core/Core/IOS/Starlet/StarletMemory.h @@ -0,0 +1,273 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 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; + + static bool IsMemoryAddress(u32 address); + static bool IsSRAMWindowAddress(u32 address); + static bool IsParityOdd(u8 value); + static std::array 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 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 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 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* buffer, size_t* actual_length); + std::vector GetOHCI1Descriptor(u16 value) const; + void ExecuteOHCI1HCICommand(const std::vector& command); + void QueueOHCI1HCIEvent(u8 event_code, const void* parameters, size_t parameter_size); + void QueueOHCI1HCICommandComplete(u16 opcode, const std::vector& 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* 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 m_boot_rom{}; + std::array m_keys{}; + std::array m_sram{}; + File::IOFile m_nand; + File::IOFile m_sd_card; + std::map m_registers; + std::map 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 m_wifi_sdio_registers; + std::deque 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 m_ohci_interrupt_enable{}; + std::array m_ohci_interrupt_status_before_write{}; + std::array, 2> m_ohci_port_status_before_write{}; + std::array, 2> m_ohci_port_reset_frames{}; + std::array m_ohci_frame_cycle_remainder{}; + std::array m_ohci_operational{}; + u16 m_ohci1_attach_delay_frames = 0; + std::array m_ohci1_setup_packet{}; + std::vector m_ohci1_control_response; + std::deque> m_ohci1_hci_events; + std::deque> m_ohci1_acl_packets; + u8 m_ohci1_acl_delay_frames = 0; + std::array, 5> m_wiimotes; + std::array m_wiimote_addresses{}; + u64 m_wiimote_cycle_remainder = 0; + std::array 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 m_aes_key{}; + std::array m_aes_iv{}; + std::array m_sha_state{}; + std::array m_ddr_registers{}; + std::array m_ddr_seq_registers{}; + std::array m_ddr_bist_registers{}; + std::array 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 diff --git a/Source/Core/Core/IOS/USB/Bluetooth/BTBase.cpp b/Source/Core/Core/IOS/USB/Bluetooth/BTBase.cpp index 51151b739a..47f416d4b2 100644 --- a/Source/Core/Core/IOS/USB/Bluetooth/BTBase.cpp +++ b/Source/Core/Core/IOS/USB/Bluetooth/BTBase.cpp @@ -3,6 +3,10 @@ #include "Core/IOS/USB/Bluetooth/BTBase.h" +#include +#include +#include +#include #include #include @@ -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 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 GetEmulatedWiimotePairingData() +{ + ConfPads bt_dinf{}; + + for (u8 i = 0; i != MAX_BBMOTES; ++i) + { + const std::array 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 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 diff --git a/Source/Core/Core/IOS/USB/Bluetooth/BTBase.h b/Source/Core/Core/IOS/USB/Bluetooth/BTBase.h index 7a93f4f7d4..f1e7cc5467 100644 --- a/Source/Core/Core/IOS/USB/Bluetooth/BTBase.h +++ b/Source/Core/Core/IOS/USB/Bluetooth/BTBase.h @@ -3,6 +3,8 @@ #pragma once +#include + #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 GetEmulatedWiimotePairingData(); +void PopulateEmulatedWiimotePairings(SysConf* sysconf); class BluetoothBaseDevice : public EmulationDevice { diff --git a/Source/Core/Core/IOS/USB/Bluetooth/BTEmu.cpp b/Source/Core/Core/IOS/USB/Bluetooth/BTEmu.cpp index 000883ec3f..6b6ba8faf8 100644 --- a/Source/Core/Core/IOS/USB/Bluetooth/BTEmu.cpp +++ b/Source/Core/Core/IOS/USB/Bluetooth/BTEmu.cpp @@ -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(this, tmp_bd, hid_source_number); + m_wiimotes[i] = std::make_unique(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 BluetoothEmuDevice::Close(u32 fd) { // Clean up state diff --git a/Source/Core/Core/IOS/USB/Bluetooth/BTEmu.h b/Source/Core/Core/IOS/USB/Bluetooth/BTEmu.h index 70735d0da8..c93030a107 100644 --- a/Source/Core/Core/IOS/USB/Bluetooth/BTEmu.h +++ b/Source/Core/Core/IOS/USB/Bluetooth/BTEmu.h @@ -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 diff --git a/Source/Core/Core/IOS/USB/Bluetooth/WiimoteDevice.cpp b/Source/Core/Core/IOS/USB/Bluetooth/WiimoteDevice.cpp index 069cd0dc9c..1ef3a85e04 100644 --- a/Source/Core/Core/IOS/USB/Bluetooth/WiimoteDevice.cpp +++ b/Source/Core/Core/IOS/USB/Bluetooth/WiimoteDevice.cpp @@ -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(&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 diff --git a/Source/Core/Core/IOS/USB/Bluetooth/WiimoteDevice.h b/Source/Core/Core/IOS/USB/Bluetooth/WiimoteDevice.h index f74bb4717e..9da094464c 100644 --- a/Source/Core/Core/IOS/USB/Bluetooth/WiimoteDevice.h +++ b/Source/Core/Core/IOS/USB/Bluetooth/WiimoteDevice.h @@ -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; using LinkKeyType = std::array; - 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; - 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; diff --git a/Source/Core/Core/PowerPC/MMU.cpp b/Source/Core/Core/PowerPC/MMU.cpp index 940e93c7ea..d8a95e7ef1 100644 --- a/Source/Core/Core/PowerPC/MMU.cpp +++ b/Source/Core/Core/PowerPC/MMU.cpp @@ -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(starlet->ReadHollywoodBus8(em_address)); + if constexpr (sizeof(T) == sizeof(u16)) + return static_cast(starlet->ReadHollywoodBus16(em_address)); + if constexpr (sizeof(T) == sizeof(u32)) + return static_cast(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 +template 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(em_address, std::rotr(data, second_half_size * 8), - first_half_size); - WriteToHardware(em_address_end_page, data, second_half_size); + WriteToHardware( + em_address, std::rotr(data, second_half_size * 8), first_half_size); + WriteToHardware(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(data)); + return; + case 2: + starlet->WriteHollywoodBus16(em_address, static_cast(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(addr, rotated_data, 4); - WriteToHardware(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(addr, rotated_data, 4); + WriteToHardware(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 instruction = starlet->TryReadBroadwayResetInstruction(address)) + { + return TryReadInstResult{true, true, *instruction, address}; + } + } + } + bool from_bat = true; if (m_ppc_state.msr.IR) { diff --git a/Source/Core/Core/PowerPC/MMU.h b/Source/Core/Core/PowerPC/MMU.h index 8c12ead73c..5e4c765102 100644 --- a/Source/Core/Core/PowerPC/MMU.h +++ b/Source/Core/Core/PowerPC/MMU.h @@ -344,7 +344,7 @@ private: template T ReadFromHardware(u32 em_address); - template + template void WriteToHardware(u32 em_address, u32 data, u32 size); template bool IsEffectiveRAMAddress(u32 address); diff --git a/Source/Core/Core/System.cpp b/Source/Core/Core/System.cpp index 12da7dcaea..310b6c596c 100644 --- a/Source/Core/Core/System.cpp +++ b/Source/Core/Core/System.cpp @@ -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 m_ios; + std::unique_ptr m_starlet; AudioInterface::AudioInterfaceManager m_audio_interface; CoreTiming::CoreTimingManager m_core_timing; @@ -162,6 +164,16 @@ void System::SetIOS(std::unique_ptr 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 starlet) +{ + m_impl->m_starlet = std::move(starlet); +} + AudioInterface::AudioInterfaceManager& System::GetAudioInterface() const { return m_impl->m_audio_interface; diff --git a/Source/Core/Core/System.h b/Source/Core/Core/System.h index 8524003400..5684905398 100644 --- a/Source/Core/Core/System.h +++ b/Source/Core/Core/System.h @@ -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); + IOS::LLE::Starlet* GetStarlet() const; + void SetStarlet(std::unique_ptr starlet); AudioInterface::AudioInterfaceManager& GetAudioInterface() const; CPU::CPUManager& GetCPU() const; diff --git a/Source/Core/InputCommon/ControllerInterface/DInput/DInputKeyboardMouse.cpp b/Source/Core/InputCommon/ControllerInterface/DInput/DInputKeyboardMouse.cpp index 92ebf355fc..8518ceb832 100644 --- a/Source/Core/InputCommon/ControllerInterface/DInput/DInputKeyboardMouse.cpp +++ b/Source/Core/InputCommon/ControllerInterface/DInput/DInputKeyboardMouse.cpp @@ -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); diff --git a/Source/UnitTests/Core/CMakeLists.txt b/Source/UnitTests/Core/CMakeLists.txt index 30aeae4477..9770adac5a 100644 --- a/Source/UnitTests/Core/CMakeLists.txt +++ b/Source/UnitTests/Core/CMakeLists.txt @@ -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) diff --git a/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp b/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp new file mode 100644 index 0000000000..4d51e20be1 --- /dev/null +++ b/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp @@ -0,0 +1,841 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include + +#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((static_cast(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(m_memory[offset]) << 24) | + (static_cast(m_memory[offset + 1]) << 16) | + (static_cast(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(instruction >> 24); + m_memory[offset + 1] = static_cast(instruction >> 16); + m_memory[offset + 2] = static_cast(instruction >> 8); + m_memory[offset + 3] = static_cast(instruction); + } + + void WriteThumb(u32 address, u16 instruction) + { + const size_t offset = ToOffset(address); + m_memory[offset] = static_cast(instruction >> 8); + m_memory[offset + 1] = static_cast(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 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(memory.Read8(address)) << 24 | + static_cast(memory.Read8(address + 1)) << 16 | + static_cast(memory.Read8(address + 2)) << 8 | memory.Read8(address + 3); + }; + const auto write_word = [&memory](u32 address, u32 value) { + memory.Write8(address, static_cast(value >> 24)); + memory.Write8(address + 1, static_cast(value >> 16)); + memory.Write8(address + 2, static_cast(value >> 8)); + memory.Write8(address + 3, static_cast(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(memory.Read8(address)) << 24 | + static_cast(memory.Read8(address + 1)) << 16 | + static_cast(memory.Read8(address + 2)) << 8 | memory.Read8(address + 3); + }; + const auto write_word = [&memory](u32 address, u32 value) { + memory.Write8(address, static_cast(value >> 24)); + memory.Write8(address + 1, static_cast(value >> 16)); + memory.Write8(address + 2, static_cast(value >> 8)); + memory.Write8(address + 3, static_cast(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(memory.Read8(address)) << 24 | + static_cast(memory.Read8(address + 1)) << 16 | + static_cast(memory.Read8(address + 2)) << 8 | memory.Read8(address + 3); + }; + const auto write_word = [&memory](u32 address, u32 value) { + memory.Write8(address, static_cast(value >> 24)); + memory.Write8(address + 1, static_cast(value >> 16)); + memory.Write8(address + 2, static_cast(value >> 8)); + memory.Write8(address + 3, static_cast(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(memory.Read8(address)) << 24 | + static_cast(memory.Read8(address + 1)) << 16 | + static_cast(memory.Read8(address + 2)) << 8 | memory.Read8(address + 3); + }; + const auto write_word = [&memory](u32 address, u32 value) { + memory.Write8(address, static_cast(value >> 24)); + memory.Write8(address + 1, static_cast(value >> 16)); + memory.Write8(address + 2, static_cast(value >> 8)); + memory.Write8(address + 3, static_cast(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(memory.Read8(address)) << 24 | + static_cast(memory.Read8(address + 1)) << 16 | + static_cast(memory.Read8(address + 2)) << 8 | memory.Read8(address + 3); + }; + const auto write_word = [&memory](u32 address, u32 value) { + memory.Write8(address, static_cast(value >> 24)); + memory.Write8(address + 1, static_cast(value >> 16)); + memory.Write8(address + 2, static_cast(value >> 8)); + memory.Write8(address + 3, static_cast(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(memory.Read8(address)) << 24 | + static_cast(memory.Read8(address + 1)) << 16 | + static_cast(memory.Read8(address + 2)) << 8 | memory.Read8(address + 3); + }; + const auto write_word = [&memory](u32 address, u32 value) { + memory.Write8(address, static_cast(value >> 24)); + memory.Write8(address + 1, static_cast(value >> 16)); + memory.Write8(address + 2, static_cast(value >> 8)); + memory.Write8(address + 3, static_cast(value)); + }; + const auto read_le_word = [&memory](u32 address) { + return static_cast(memory.Read8(address)) | + static_cast(memory.Read8(address + 1)) << 8 | + static_cast(memory.Read8(address + 2)) << 16 | + static_cast(memory.Read8(address + 3)) << 24; + }; + const auto write_le_word = [&memory](u32 address, u32 value) { + memory.Write8(address, static_cast(value)); + memory.Write8(address + 1, static_cast(value >> 8)); + memory.Write8(address + 2, static_cast(value >> 16)); + memory.Write8(address + 3, static_cast(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 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(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(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(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(ARMCore::Mode::System)); + core.SetRegister(13, 0x11111111); + core.SetRegister(14, 0x22222222); + core.SetCPSR(static_cast(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(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(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(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(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(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(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(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 diff --git a/Tools/Dump-Process-Memory.ps1 b/Tools/Dump-Process-Memory.ps1 new file mode 100644 index 0000000000..077bf39280 --- /dev/null +++ b/Tools/Dump-Process-Memory.ps1 @@ -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) +} diff --git a/Tools/Dump-Starlet-SRAM.ps1 b/Tools/Dump-Starlet-SRAM.ps1 new file mode 100644 index 0000000000..3211d86346 --- /dev/null +++ b/Tools/Dump-Starlet-SRAM.ps1 @@ -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(); + 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 diff --git a/Tools/Generate-LetterBomb.py b/Tools/Generate-LetterBomb.py new file mode 100644 index 0000000000..149c561c8d --- /dev/null +++ b/Tools/Generate-LetterBomb.py @@ -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() diff --git a/Tools/List-Process-MemoryRegions.ps1 b/Tools/List-Process-MemoryRegions.ps1 new file mode 100644 index 0000000000..5e56943aef --- /dev/null +++ b/Tools/List-Process-MemoryRegions.ps1 @@ -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(); + try + { + ulong address = 0x10000; + const ulong maximum = 0x00007fffffff0000UL; + int size = Marshal.SizeOf(); + 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 + } +} diff --git a/Tools/Read-Process-Memory.ps1 b/Tools/Read-Process-Memory.ps1 new file mode 100644 index 0000000000..78165dc232 --- /dev/null +++ b/Tools/Read-Process-Memory.ps1 @@ -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 +} diff --git a/Tools/disassemble-process-memory.py b/Tools/disassemble-process-memory.py new file mode 100644 index 0000000000..61daedfe86 --- /dev/null +++ b/Tools/disassemble-process-memory.py @@ -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() diff --git a/docs/GPU_Emulation.md b/docs/GPU_Emulation.md new file mode 100644 index 0000000000..5eadf354cd --- /dev/null +++ b/docs/GPU_Emulation.md @@ -0,0 +1,1004 @@ +# Émulation du GPU GameCube/Wii dans Dolphin + +> État du code documenté : `38e70fda6597dab6b7e5e4d3949b199c0b4f2244`. +> +> Cette carte décrit le comportement du dépôt à cette révision. Elle distingue systématiquement +> le matériel invité, l'état émulé, les optimisations de Dolphin et le travail confié au GPU hôte. + +## 1. Résumé en une phrase + +Dolphin n'émule pas un jeu d'instructions de shader : il reçoit le flux de commandes fixe du GPU +GX, reproduit ses trois banques d'état CP/XF/BP, convertit les sommets, traduit la transformation et +le TEV en shaders ou en calcul logiciel, rend dans une EFB émulée, copie cette EFB vers la RAM/XFB, +puis laisse la VI émulée décider quand présenter l'image. + +Cette distinction est fondamentale : le GPU invité est une machine à états alimentée par FIFO, +alors que le GPU hôte exécute des pipelines modernes créés à partir d'un instantané de cet état. + +## 2. Vocabulaire et frontières + +| Terme | Sens dans cette documentation | +|---|---| +| PPC / CPU invité | Le processeur PowerPC émulé qui exécute le jeu et écrit les commandes GX. | +| Gather pipe | Tampon matériel de 32 octets alimenté par les stores PPC. | +| PI FIFO | Vue CPU du FIFO circulaire en RAM, gérée par le Processor Interface. | +| CP | Command Processor : registres du FIFO et état décrivant les données de sommets. | +| XF | Transform Unit : matrices, éclairage, viewport, projection et génération de coordonnées. | +| BP | Registres raster/TEV/Pixel Engine envoyés dans le flux de commandes. | +| TEV | Texture Environment Unit fixe, jusqu'à 16 étages de combinaison couleur/alpha. | +| EFB | Embedded Frame Buffer natif de `640 × 528`, couleur et profondeur. | +| XFB | External Frame Buffer en RAM, lu par la Video Interface pour l'affichage. | +| VI | Video Interface : timings de champs/lignes, adresses XFB et déclenchement de la présentation. | +| Backend matériel | D3D11, D3D12, Metal, OpenGL ou Vulkan ; traduit l'état GX vers l'API hôte. | +| Backend logiciel | Transforme, découpe, rastérise et exécute le TEV sur le CPU hôte. | + +Les constantes de dimensions sont définies dans +[`VideoCommon.h`](../Source/Core/VideoCommon/VideoCommon.h) : EFB `640 × 528`, XFB maximal +`720 × 576`. + +## 3. Carte d'ensemble + +```mermaid +flowchart LR + PPC["PPC invité
stores GX"] --> MMU["MMU
détection WPAR"] + MMU --> GP["Gather pipe
32 octets"] + GP --> RAMFIFO["FIFO circulaire
en RAM invitée"] + GP --> CPREG["Registres CP
pointeurs, distance,
watermarks"] + RAMFIFO --> FBUF["Tampon vidéo Dolphin
2 MiB"] + CPREG --> FBUF + FBUF --> DEC["OpcodeDecoder"] + + DEC --> CPM["État CP
VCD, VAT, arrays"] + DEC --> XFM["Mémoire XF
matrices, éclairage,
projection"] + DEC --> BPM["Mémoire BP
raster, textures,
TEV, copies"] + DEC --> VL["VertexLoader"] + + CPM --> VL + XFM --> PIPE["Constantes et shaders"] + BPM --> PIPE + VL --> VM["VertexManager
batch + indices"] + VM --> PIPE + BPM --> TC["TextureCache + TMEM"] + TC --> PIPE + PIPE --> GFX["AbstractGfx"] + GFX --> HOST["API / GPU hôte
ou rasteriseur logiciel"] + HOST --> EFB["EFB émulée"] + + BPM --> COPY["Copie EFB"] + EFB --> COPY + COPY --> XFB["RAM XFB et/ou
copie VRAM"] + XFB --> VI["Video Interface"] + VI --> PRES["Presenter"] + PRES --> SCREEN["Backbuffer / écran"] + + CPREG -. "IRQ CP" .-> PPC + BPM -. "token / finish" .-> PE["Pixel Engine MMIO"] + PE -. "IRQ PE" .-> PPC + PPC -. "peek / poke EFB" .-> EFB +``` + +La partie située avant `AbstractGfx` décide **ce que le GPU invité doit faire**. La partie située +après `AbstractGfx` décide **comment l'API graphique disponible peut le faire**. + +## 4. Carte des composants du code + +| Responsabilité | Fichiers principaux | Points d'entrée | +|---|---|---| +| Détection des writes gather pipe | [`MMU.cpp`](../Source/Core/Core/PowerPC/MMU.cpp), [`GPFifo.cpp`](../Source/Core/Core/HW/GPFifo.cpp) | `MMU::WriteToHardware`, `GPFifoManager::Write*`, `UpdateGatherPipe` | +| Vue CPU du FIFO | [`ProcessorInterface.cpp`](../Source/Core/Core/HW/ProcessorInterface.cpp) | registres `PI_FIFO_*` | +| Vue GPU du FIFO et IRQ CP | [`CommandProcessor.cpp`](../Source/Core/VideoCommon/CommandProcessor.cpp) | `RegisterMMIO`, `GatherPipeBursted`, `SetCPStatusFromCPU/GPU` | +| Ordonnancement CPU/GPU | [`Fifo.cpp`](../Source/Core/VideoCommon/Fifo.cpp) | `RunGpuLoop`, `RunGpuOnCpu`, `SyncGPU`, `WaitForGpuThread` | +| Syntaxe du flux GX | [`OpcodeDecoding.h`](../Source/Core/VideoCommon/OpcodeDecoding.h), [`OpcodeDecoding.cpp`](../Source/Core/VideoCommon/OpcodeDecoding.cpp) | `Run`, `RunCommand`, `RunFifo` | +| État CP | [`CPMemory.h`](../Source/Core/VideoCommon/CPMemory.h), [`CPMemory.cpp`](../Source/Core/VideoCommon/CPMemory.cpp) | `CPState::LoadCPReg` | +| État XF | [`XFMemory.h`](../Source/Core/VideoCommon/XFMemory.h), [`XFStructs.cpp`](../Source/Core/VideoCommon/XFStructs.cpp), [`XFStateManager.cpp`](../Source/Core/VideoCommon/XFStateManager.cpp) | `LoadXFReg`, `LoadIndexedXF`, `InvalidateXFRange` | +| État BP et effets de bord | [`BPMemory.h`](../Source/Core/VideoCommon/BPMemory.h), [`BPStructs.cpp`](../Source/Core/VideoCommon/BPStructs.cpp), [`BPFunctions.cpp`](../Source/Core/VideoCommon/BPFunctions.cpp) | `LoadBPReg`, `BPWritten` | +| Conversion des sommets | [`VertexLoaderManager.cpp`](../Source/Core/VideoCommon/VertexLoaderManager.cpp), [`VertexLoader.cpp`](../Source/Core/VideoCommon/VertexLoader.cpp) | `RefreshLoader`, `RunVertices` | +| Assemblage et draw | [`VertexManagerBase.cpp`](../Source/Core/VideoCommon/VertexManagerBase.cpp), [`IndexGenerator.cpp`](../Source/Core/VideoCommon/IndexGenerator.cpp) | `PrepareForAdditionalData`, `Flush`, `RenderDrawCall` | +| Traduction en shaders | [`VertexShaderGen.cpp`](../Source/Core/VideoCommon/VertexShaderGen.cpp), [`PixelShaderGen.cpp`](../Source/Core/VideoCommon/PixelShaderGen.cpp), [`ShaderCache.cpp`](../Source/Core/VideoCommon/ShaderCache.cpp) | `Get*ShaderUid`, `Generate*ShaderCode`, `GetPipelineForUid` | +| Textures et TMEM | [`TextureCacheBase.cpp`](../Source/Core/VideoCommon/TextureCacheBase.cpp), [`TextureInfo.cpp`](../Source/Core/VideoCommon/TextureInfo.cpp), [`TMEM.cpp`](../Source/Core/VideoCommon/TMEM.cpp) | `Load`, `GetTexture`, `BindTextures`, `TMEM::Bind` | +| EFB | [`FramebufferManager.cpp`](../Source/Core/VideoCommon/FramebufferManager.cpp), [`EFBInterface.cpp`](../Source/Core/VideoCommon/EFBInterface.cpp) | `BindEFBFramebuffer`, `PeekEFB*`, `PokeEFB*`, `ClearEFB` | +| Copies EFB/XFB | [`BPStructs.cpp`](../Source/Core/VideoCommon/BPStructs.cpp), [`TextureCacheBase.cpp`](../Source/Core/VideoCommon/TextureCacheBase.cpp) | `BPMEM_TRIGGER_EFB_COPY`, `CopyRenderTargetToTexture` | +| Tokens, finish et IRQ PE | [`PixelEngine.cpp`](../Source/Core/VideoCommon/PixelEngine.cpp) | `SetToken`, `SetFinish`, `UpdateInterrupts` | +| Scanout VI | [`VideoInterface.cpp`](../Source/Core/Core/HW/VideoInterface.cpp) | `Update`, `BeginField`, `EndField`, `OutputField` | +| Présentation | [`VideoBackendBase.cpp`](../Source/Core/VideoCommon/VideoBackendBase.cpp), [`Present.cpp`](../Source/Core/VideoCommon/Present.cpp) | `Video_OutputXFB`, `ViSwap`, `Present` | +| Frontière API hôte | [`AbstractGfx.h`](../Source/Core/VideoCommon/AbstractGfx.h) | `CreatePipeline`, `SetPipeline`, `DrawIndexed`, `PresentBackbuffer` | +| Sauvegarde de l'état vidéo | [`VideoState.cpp`](../Source/Core/VideoCommon/VideoState.cpp) | `VideoCommon_DoState` | + +## 5. Initialisation et modèle de threads + +### 5.1 Création du backend + +[`Core.cpp`](../Source/Core/Core/Core.cpp) initialise le backend sur le thread qui l'utilisera. +Cette contrainte est notamment requise par OpenGL. + +[`VideoBackendBase::InitializeShared`](../Source/Core/VideoCommon/VideoBackendBase.cpp) construit les +objets communs : `AbstractGfx`, `VertexManagerBase`, cache de shaders, `FramebufferManager`, cache de +textures, `Presenter`, compteurs de performance et bounding box. Il initialise ensuite CP, FIFO, +PE, BP, chargeurs de sommets, managers de constantes XF et TMEM. + +### 5.2 Les trois chemins d'exécution + +| Mode | Propriétaire du décodage et du rendu | Fonction centrale | Particularité | +|---|---|---|---| +| Single Core | Thread CPU-GPU unique | `FifoManager::RunGpuOnCpu` | Le temps GPU est consommé par des événements `CoreTiming`; les requêtes vidéo sont exécutées directement. | +| Dual Core normal | Thread vidéo dédié | `FifoManager::RunGpuLoop` | Le CPU produit le FIFO pendant que le thread vidéo le consomme. | +| Dual Core déterministe | CPU pour la copie/prélecture, thread vidéo pour le décodage réel | `RunGpuOnCpu` + branche déterministe de `RunGpuLoop` | Un préprocesseur CPU fige les dépendances mémoire et maintient un second état CP. | + +Le réglage « Synchronize GPU Thread » est orthogonal. Quand il est actif, `m_sync_ticks` mesure la +distance temporelle émulée : le GPU est réveillé au seuil minimal et le CPU attend au seuil maximal. +`MAIN_SYNC_GPU_OVERCLOCK` met à l'échelle le coût estimé des commandes. + +### 5.3 Pourquoi le mode déterministe prétraite le FIFO + +Lire une display list ou une source d'indexed XF load directement dans la RAM invitée depuis le +thread vidéo crée une course : le CPU peut modifier cette RAM avant que le GPU émulé ne l'ait lue. +Le mode déterministe résout ce problème ainsi : + +1. le CPU copie chaque burst de 32 octets dans `m_video_buffer` ; +2. `RunFifo` décode juste assez pour maintenir `g_preprocess_cp_state` ; +3. les display lists et indexed XF loads sont copiés dans `m_fifo_aux_data` ; +4. le thread vidéo exécute `RunFifo` et consomme ces instantanés plutôt que la RAM devenue + mutable ; +5. les commandes PE `token`/`finish` sont planifiées par le préprocesseur, puis ne le sont pas une + seconde fois par le décodage principal. + +`g_main_cp_state` est donc l'état qui rend ; `g_preprocess_cp_state` est l'état qui permet de calculer +la taille des futures commandes et leurs dépendances mémoire. + +### 5.4 Frontière des requêtes asynchrones + +[`AsyncRequests`](../Source/Core/VideoCommon/AsyncRequests.cpp) transporte vers le thread vidéo les +opérations initiées côté CPU : présentation, lectures EFB, résultats de performance, bounding box et +savestates. Avant de vider cette file, `PullEvents` appelle `g_vertex_manager->Flush()` afin qu'une +lecture observe tous les draws antérieurs. + +Une requête bloquante attend son résultat. Une requête non bloquante réveille le FIFO et sera traitée +au prochain passage du thread vidéo. En Single Core, le mode `passthrough` exécute directement le +callback. + +### 5.5 Quatre opérations souvent confondues + +| Opération | Ce qu'elle attend ou soumet | +|---|---| +| `VertexManagerBase::Flush` | Termine le batch GX courant et émet éventuellement un draw hôte. | +| `FifoManager::FlushGpu` | Attend que la boucle FIFO du thread vidéo atteigne un point de repos. | +| `AbstractGfx::Flush` | Soumet/segmente le command buffer de l'API hôte ; ne signifie pas forcément GPU hôte idle. | +| `AbstractGfx::WaitForGPUIdle` | Attend réellement l'achèvement du GPU hôte quand un backend l'implémente. | + +## 6. Du store PPC au FIFO GPU + +### 6.1 Détection du gather pipe + +Le WPAR du PPC pointe habituellement sur l'adresse physique `0x0C008000`, constante +`GATHER_PIPE_PHYSICAL_ADDRESS`. [`MMU.cpp`](../Source/Core/Core/PowerPC/MMU.cpp) reconnaît les stores +non cachés correspondants et les redirige vers `GPFifoManager::Write8/16/32/64`. + +Les écritures multi-octets sont converties en big-endian avant d'être ajoutées au tampon, de sorte +que le flux mémoire corresponde au format GX. Les JIT x64 et ARM64 possèdent des chemins rapides qui +peuvent différer le contrôle de remplissage jusqu'à la fin d'un bloc compilé. + +### 6.2 Burst de 32 octets + +À chaque bloc complet : + +1. `GPFifoManager::UpdateGatherPipe` copie 32 octets vers + `ProcessorInterface::m_fifo_cpu_write_pointer` en RAM invitée ; +2. le pointeur PI avance de 32 octets ou reboucle de `end` vers `base` ; +3. `CommandProcessorManager::GatherPipeBursted` met à jour la vue CP si PI et CP sont liés ; +4. le FIFO vidéo est réveillé ; +5. les octets excédentaires du tampon gather sont ramenés au début. + +Le tampon alloué accepte jusqu'à 16 bursts afin que les chemins JIT rapides puissent grouper les +writes. Le bit WPAR `BNE` n'est pas réellement émulé : `IsBNE()` renvoie toujours `false` pour éviter +les blocages de logiciels utilisant les display lists. + +### 6.3 Deux vues du même anneau + +Le PI expose la base, la fin et le write pointer CPU. Le CP expose en plus le read pointer, la +distance lecture-écriture, les seuils haut/bas et un breakpoint. Quand `GPLinkEnable` vaut 1, +`GatherPipeBursted` maintient les pointeurs PI et CP identiques, avance `CPWritePointer` et ajoute 32 +à `CPReadWriteDistance`. + +Les adresses CP sont alignées sur 32 octets. Le masque physique est `0x03ffffff` sur GameCube et +`0x1fffffff` sur Wii. La fin de l'anneau est inclusive dans la logique de pointeur : un pointeur égal +à `CPEnd` reboucle vers `CPBase` au burst suivant. + +Si les FIFO ne sont pas liés, le burst reste écrit par le PI en RAM, mais le CP n'avance pas +automatiquement. Dolphin réveille tout de même le GPU et protège un cas de double-buffering en Dual +Core par un `FlushGpu` ciblé. + +### 6.4 Statut, watermarks, breakpoint et interruption CP + +`SetCPStatusFromCPU/GPU` calcule : + +- overflow si `CPReadWriteDistance > CPHiWatermark` ; +- underflow si `CPReadWriteDistance < CPLoWatermark` ; +- breakpoint si `CPReadPointer == CPBreakpoint` et le contrôle l'autorise. + +Chaque cause est combinée avec son bit d'activation, puis l'ensemble est encore conditionné par +`GPReadEnable`. L'interruption CP utilise la cause PI `0x800`. En Dual Core, le thread vidéo planifie +sa modification sur le thread CPU via `CoreTiming` et se bloque temporairement avec +`m_interrupt_waiting` afin de préserver l'ordre. + +Une lecture du registre de statut synchronise d'abord le GPU. En Dual Core, les lectures publiques +du read pointer et de la distance utilisent `SafeCPReadPointer`, qui n'est avancé que lorsque tous les +octets déjà copiés dans le tampon Dolphin ont été consommés. Cela évite d'annoncer au jeu qu'une +commande partielle est terminée. + +Limites de cette interface : + +- le registre CP clear est intentionnellement sans effet ; +- la plupart des métriques CP renvoient zéro, `CLKS_PER_VTX_OUT` renvoie 4 ; +- les timings de FIFO ne sont pas une simulation cycle par cycle. + +## 7. Consommation du FIFO + +### 7.1 Tampon intermédiaire + +Le thread consommateur lit la RAM invitée par blocs de 32 octets et les copie dans +`m_video_buffer`, un tampon de 2 MiB avec 4 octets de marge pour les overreads SIMD. Ce second tampon +est nécessaire parce qu'une commande GX peut traverser la frontière d'un burst. + +Après chaque burst en mode normal : + +1. `OpcodeDecoder::RunFifo` consomme toutes les commandes complètes disponibles ; +2. `CPReadPointer` avance ou reboucle ; +3. `CPReadWriteDistance` diminue de 32 ; +4. le statut CP et les interruptions sont recalculés ; +5. les requêtes asynchrones sont traitées. + +Quand le FIFO devient vide, le `VertexManager` est flushé et le cache de peeks EFB peut être +rafraîchi. Vider le FIFO n'implique donc pas qu'un batch reste indéfiniment en attente. + +### 7.2 Séquence Dual Core normale + +```mermaid +sequenceDiagram + participant CPU as Thread CPU + participant GP as Gather pipe + participant RAM as FIFO en RAM + participant CP as Command Processor + participant GPU as Thread vidéo + participant OD as OpcodeDecoder + participant VM as VertexManager + participant API as API/GPU hôte + + CPU->>GP: store GX + loop chaque tranche complète de 32 octets + GP->>RAM: CopyToEmu(write_ptr, 32) + GP->>CP: GatherPipeBursted() + CP->>CP: write_ptr += 32, distance += 32 + CP-->>GPU: réveil RunGpu() + end + GPU->>RAM: CopyFromEmu(read_ptr, 32) + GPU->>OD: RunFifo(octets disponibles) + alt commande complète + OD->>VM: état ou primitive + else commande partielle + OD-->>GPU: 0 octet consommé + end + GPU->>CP: read_ptr += 32, distance -= 32 + VM->>API: Flush puis DrawIndexed +``` + +## 8. Grammaire du flux GX + +[`OpcodeDecoding.h`](../Source/Core/VideoCommon/OpcodeDecoding.h) contient la grammaire commune. +`RunCommand` renvoie zéro si la commande n'est pas encore entière ; aucun octet n'est alors perdu. + +| Opcode | Taille encodée | Effet | Coût estimé dans `RunCallback` | +|---|---:|---|---:| +| `0x00` NOP | 1 par NOP | Fusionne une suite de NOP en un callback. | 6 cycles par NOP | +| `0x08` LOAD_CP | 6 | Sous-commande 8 bits + valeur big-endian 32 bits. | 12 | +| `0x10` LOAD_XF | `5 + 4 × n`, `n=1..16` | Adresse XF 16 bits + `n` mots 32 bits. | `18 + 6 × n` | +| `0x20/28/30/38` LOAD_INDX | 5 | Copie indexée vers XF via les arrays CP A/B/C/D. | 6 | +| `0x40` CALL_DL | 9 | Adresse + taille, toutes deux forcées à l'alignement 32. | 6 + contenu | +| `0x44` métriques | 1 | Reconnue et journalisée, sans sémantique de métrique. | 6 | +| `0x48` invalidation vertex cache | 1 | Reconnue et journalisée, sans cache de sommets matériel à invalider. | 6 | +| `0x61` LOAD_BP | 5 | Adresse BP 8 bits + valeur big-endian 24 bits. | 12 | +| `0x80..0xBF` primitive | `3 + count × vertex_size` | Type dans les bits 3..6, VAT dans les bits 0..2, count 16 bits. | `12 × count + 6` | +| autre | 1 | Avertissement/erreur selon l'opcode. | 1 | + +Le `vertex_size` n'est pas présent dans la commande : il est dérivé de l'état CP courant. C'est la +raison pour laquelle une display list ne peut pas être précompilée indépendamment de l'état qui la +précède. + +Les display lists sont interprétées récursivement avec le même callback, mais un booléen interdit +l'imbrication récursive. En mode déterministe, leur contenu est capturé lors du prétraitement. En +mode normal, il est lu dans la RAM au moment de l'exécution par le thread vidéo. + +Lors d'un enregistrement FIFO, les commandes internes d'une display list sont aplaties dans le flux +enregistré ; le `CALL_DL` lui-même n'est pas écrit une seconde fois. + +## 9. Les trois banques d'état + +### 9.1 CP : comment lire les sommets + +`CPState` contient : + +- deux registres d'indices de matrices ; +- le Vertex Component Descriptor (`VCD`) indiquant, pour chaque attribut, absent/direct/index 8/index + 16 ; +- huit Vertex Attribute Tables (`VAT`) décrivant nombre de composantes, type, fraction et format de + couleur ; +- 16 bases et strides d'arrays, dont 12 pour les attributs de sommets et 4 pour les indexed XF + loads. + +| Groupe CP | Commandes | Rôle | +|---|---|---| +| Matrices | `0x30`, `0x40` | Matrice position/normale et matrices texture 0..7. | +| VCD | `0x50`, `0x60` | Présence et adressage de position, normale, couleurs, texcoords et indices de matrices. | +| VAT A/B/C | `0x70..0x77`, `0x80..0x87`, `0x90..0x97` | Huit formats complets de sommet, répartis sur trois groupes. | +| Array base | `0xA0..0xAF` | Adresse physique de chaque array. | +| Array stride | `0xB0..0xBF` | Pas 8 bits de chaque array. | + +Une modification VCD/VAT marque les chargeurs concernés comme sales. Une modification de base +invalide les pointeurs d'arrays résolus. Les changements de matrices sont transmis au +`XFStateManager`, qui flushe si nécessaire le batch courant. + +### 9.2 XF : transformer les sommets + +La structure `XFMemory` reproduit l'espace d'adressage XF : + +| Plage XF | Contenu | +|---|---| +| `0x0000..0x00FF` | Matrices de position/transformation. | +| `0x0400..0x045F` | Matrices normales. | +| `0x0500..0x05FF` | Matrices post-texture. | +| `0x0600..0x067F` | Huit lumières. | +| `0x1000..0x1057` | Registres : vertex spec, canaux, matériaux, viewport, projection, texgen. | + +`LoadXFReg` peut traverser la frontière mémoire/registres. Les mots sont convertis depuis le +big-endian. Une écriture mémoire flushe le batch puis invalide précisément les plages de constantes +touchées. Une écriture de registre flushe uniquement quand l'ancien état pourrait être utilisé par +des sommets déjà accumulés. + +Les indexed XF loads calculent : + +```text +source = cp.array_base[array] + cp.array_stride[array] × index +destination = xfmem[address .. address + size) +``` + +En Dual Core déterministe, ces octets source passent par le FIFO auxiliaire. + +### 9.3 BP : raster, textures, TEV et opérations + +`BPMemory` est un tableau logique de 256 mots de 24 bits. Les groupes les plus importants sont : + +| Adresses BP | Contenu principal | +|---|---| +| `0x00` | Nombre de texgens/canaux/étages TEV/indirects, culling, zfreeze. | +| `0x06..0x1F` | Matrices et commandes de textures indirectes. | +| `0x20..0x3F` | Scissor, lignes/points, ordres TEV, tailles texcoords. | +| `0x40..0x44` | Z test, blend/logic op, destination alpha, format EFB, field mask. | +| `0x45..0x59` | Draw done, tokens, paramètres/trigger de copie EFB, clear et bounding box. | +| `0x60..0x69` | Préchargement TMEM, TLUT, invalidation et quelques métriques/modes de champ. | +| `0x80..0xBF` | Huit unités de texture : sampling, LOD, taille, format, adresses RAM/TMEM/TLUT. | +| `0xC0..0xDF` | 16 combinateurs TEV couleur et alpha. | +| `0xE0..0xE7` | Quatre registres TEV et quatre couleurs constantes. | +| `0xE8..0xF2` | Fog range, paramètres et couleur du fog. | +| `0xF3..0xF5` | Alpha test et Z texture. | +| `0xF6..0xFD` | Sélection des constantes et tables de swizzle. | +| `0xFE` | Masque one-shot de la prochaine écriture BP. | + +`LoadBPReg` applique le masque, calcule les bits modifiés, réinitialise le masque sauf lorsqu'il est +lui-même écrit, puis appelle `BPWritten`. + +Une écriture identique est normalement ignorée. Les commandes à effets de bord — copie, token, +draw done, TLUT, invalidation, preload, clear bbox/perf — restent exécutées même si leur valeur est +inchangée. Toute autre écriture effective commence par `FlushPipeline`, donc aucun sommet accumulé +n'est rendu avec l'état BP nouveau par erreur. + +## 10. Décodage et conversion des sommets + +### 10.1 Création d'un chargeur + +`VertexLoaderUID` est dérivé du VCD et du VAT sélectionné. `VertexLoaderManager` réutilise un +`VertexLoaderBase` déjà compilé pour cet UID ou en crée un nouveau. Selon l'architecture hôte, le +chargeur est générique, x64 ou ARM64. + +Le chargeur : + +1. calcule la taille exacte d'un sommet dans le FIFO ; +2. lit chaque attribut direct ou son index 8/16 bits ; +3. résout les bases/strides CP pour les attributs indexés ; +4. convertit positions, normales, couleurs et texcoords vers un `PortableVertexDeclaration` adapté + à l'API hôte ; +5. conserve certains derniers attributs pour les comportements matériels zfreeze, normales + manquantes et emboss mapping. + +Les bases invalides ne sont résolues que si le VCD active réellement l'array correspondant, ce qui +tolère les jeux laissant des adresses poubelles dans des arrays inutilisés. + +### 10.2 Cohérence CP/XF + +Avant la première primitive après un changement pertinent, `CheckCPConfiguration` compare le nombre +de couleurs, normales et texcoords produit par CP au vertex spec attendu par XF. Il compare aussi les +indices de matrices. Dolphin journalise, déclenche des analytics et continue autant que possible ; +le matériel réel semble pouvoir se bloquer sur certaines incohérences. + +### 10.3 Batching et primitives + +`VertexManagerBase::PrepareForAdditionalData` choisit la topologie hôte, vérifie la place restante +et flushe si le type de primitive, le format ou la capacité l'exige. `IndexGenerator` convertit : + +- quads, triangles, strips et fans vers listes/strips de triangles ; +- lignes et line strips vers la topologie disponible ou vers une expansion shader ; +- points vers points natifs ou quads expansés selon les capacités. + +Le primitive restart est utilisé quand le backend le supporte. Les très grandes commandes faciles à +scinder sont découpées en groupes de 16 380 sommets. Le CPU culling optionnel peut éviter d'émettre un +batch entièrement rejeté ; `CullMode::All` continue tout de même la conversion nécessaire au +calcul de la pente zfreeze. + +Un changement de `NativeVertexFormat` force un flush. Les sommets et indices convertis sont placés +dans des stream buffers propres au backend matériel, ou dans les buffers CPU du backend logiciel. + +## 11. Traduction du pipeline fixe GX + +### 11.1 Vertex stage + +[`VertexShaderGen.cpp`](../Source/Core/VideoCommon/VertexShaderGen.cpp) transforme l'état CP/XF en un +UID et en source de shader. Le shader reproduit notamment : + +- choix global ou par sommet de la matrice position/normale ; +- transformation position et normales ; +- éclairage des deux canaux couleur/alpha avec huit lumières ; +- jusqu'à huit texgens réguliers, emboss ou dérivés de couleurs ; +- matrices texture et post-matrices ; +- projection, viewport et ajustements de profondeur. + +Le réglage pixel lighting peut déplacer une partie de l'éclairage vers le pixel shader. Le geometry +shader commun sert aux fonctions qui ne se mappent pas directement, notamment certaines expansions +ligne/point et sorties multicouches/stéréo. + +### 11.2 Pixel stage et TEV + +[`PixelShaderGen.cpp`](../Source/Core/VideoCommon/PixelShaderGen.cpp) encode dans le shader l'état BP +qui change la structure du calcul : + +- échantillonnage des textures et mipmaps ; +- jusqu'à quatre étages indirects et 16 étages TEV ; +- swizzles, registres `prev/c0/c1/c2`, constantes K et opérations compare/add/sub ; +- alpha test ; +- fog ; +- Z texture, zfreeze et emplacement early/late du test Z ; +- quantification/dithering du format EFB ; +- destination alpha, logic ops et bounding box quand nécessaire. + +Les valeurs TEV sont majoritairement manipulées comme entiers dans les shaders générés afin de +reproduire les plages et arrondis du combinateur fixe, plutôt que comme un simple mélange flottant. + +### 11.3 État fixe hôte + +`RasterizationState`, `DepthState` et `BlendingState` extraient de `bpmem` ce que l'API hôte peut +représenter directement. Le reste est injecté dans les shaders. Les différences de capacités — +dual-source blend, framebuffer fetch, early Z, logic ops, plage de profondeur inversée — sont +déclarées dans `g_backend_info` et influencent la variante générée. + +### 11.4 UIDs, cache et ubershaders + +À chaque flush, `UpdatePipelineConfig` construit deux clés : + +- `GXPipelineUid` pour les shaders spécialisés, contenant format de sommet, UID VS/GS/PS et états + raster/depth/blend ; +- `GXUberPipelineUid` pour une variante générique dont davantage d'état arrive par constantes. + +Les modes de compilation ont les comportements suivants : + +| Mode | Comportement lorsqu'un pipeline spécialisé manque | +|---|---| +| Synchronous | Compile/charge immédiatement et bloque. | +| Synchronous UberShaders | Utilise exclusivement les ubershaders. | +| Asynchronous UberShaders | Compile le spécialisé en arrière-plan et rend provisoirement avec l'ubershader. | +| Asynchronous Skip Rendering | Lance la compilation et saute le draw jusqu'à disponibilité. | + +`ShaderCache` conserve modules, pipelines et UIDs sur disque quand le backend le permet. Les managers +de constantes VS/GS/PS suivent séparément les plages sales afin de ne réenvoyer que les données XF/BP +modifiées. + +### 11.5 Émission du draw + +`VertexManagerBase::Flush` suit cet ordre : + +1. vérifie la cohérence XF/BP ; +2. charge les textures utilisées et calcule leurs samplers ; +3. met à jour les constantes et la pente Z ; +4. applique éventuellement les mods graphiques ; +5. lie textures et palettes ; +6. choisit/crée le pipeline ; +7. upload les constantes, sommets et indices ; +8. appelle `g_gfx->SetPipeline` puis `DrawIndexed` ; +9. marque les caches EFB potentiellement obsolètes. + +`AbstractGfx` ne connaît aucun registre GameCube/Wii. Par exemple, Vulkan finit par +`vkCmdDrawIndexed`, D3D11 par `ID3D11DeviceContext::DrawIndexed`, et OpenGL par la commande GL +équivalente. + +## 12. Textures, palettes et TMEM + +### 12.1 De BP à une texture hôte + +Les registres BP de chacune des huit unités définissent wrap, filtres, LOD, taille, format, adresses +RAM/TMEM et TLUT. `TextureInfo` valide et expose les niveaux de mipmap, les dimensions en blocs et les +pointeurs source. + +`TextureCacheBase::Load` : + +1. consulte l'état TMEM et le binding existant ; +2. calcule un hash des données et, si nécessaire, de la palette ; +3. cherche une entrée compatible par adresse puis par hash ; +4. réutilise, réinterprète ou applique une palette à une copie EFB existante si possible ; +5. sinon décode la texture sur CPU ou par compute shader ; +6. charge les mipmaps et crée le sampler ; +7. lie l'entrée à l'unité de texture. + +Le cache accepte plusieurs interprétations à la même adresse. Il invalide ou met à jour partiellement +les entrées recouvertes par une écriture/copie EFB. Les hashes sûrs améliorent la compatibilité au +prix du temps CPU. + +### 12.2 Mémoire TMEM concrète + +`s_tex_mem` est un tableau de 1 MiB. Les commandes de preload et TLUT copient réellement les octets +depuis la RAM invitée vers ce tableau. Une texture RGBA8 préchargée peut utiliser séparément les +bancs AR et GB. Les indexed palettes référencent aussi cette mémoire. + +### 12.3 Modèle de cache TMEM + +[`TMEM.cpp`](../Source/Core/VideoCommon/TMEM.cpp) ne simule pas chaque remplissage de ligne. Il suit, +pour chaque unité, deux bancs configurables `even` et `odd`, leur taille estimée, leur chevauchement +et trois états : `INVALID`, `VALID`, `CACHED`. + +- toutes les textures utilisent le banc even ; +- mipmapping ou texture 32 bits active aussi odd ; +- les LOD pairs/impairs ou les moitiés de canaux sont répartis entre les bancs ; +- une texture qui tient et ne chevauche aucune unité active devient `CACHED` ; +- une texture trop grande ou chevauchée reste `VALID` et devra être revalidée/hashée ; +- une invalidation BP invalide actuellement toutes les unités, car le sens exact de son paramètre + n'est pas connu. + +Ce modèle est explicitement heuristique. Il reproduit les jeux qui réutilisent volontairement une +ancienne texture encore en TMEM et ceux qui comptent sur l'éviction naturelle d'une grande texture, +sans prétendre reconstruire le contenu de cache texel par texel. + +## 13. EFB : cible de rendu et accès CPU + +### 13.1 Représentation dans les backends matériels + +`FramebufferManager` crée une texture couleur, une texture profondeur et un framebuffer hôte. La +taille est la résolution interne configurée appliquée aux `640 × 528` pixels natifs. Des textures +supplémentaires servent aux resolves MSAA, conversions de format, readbacks et caches de peeks. + +Les backends matériels utilisent normalement une couleur RGBA8 et une profondeur 24 bits ou plus, +même quand le jeu choisit RGB8, RGBA6, RGB565/Z16 ou Z-only. Dolphin réintroduit la quantification +pertinente dans les shaders, clears et lectures. Le depth buffer hôte est inversé lorsque nécessaire +pour conserver davantage de précision. + +Un changement de format EFB peut réinterpréter les bits existants par un pipeline de conversion si +`bEFBEmulateFormatChanges` est actif. Sans ce réglage, seul le nouvel état est conservé. + +### 13.2 Peek et poke via l'espace mémoire PPC + +Les accès PPC dans la plage EFB sont interceptés par `MMU.cpp`. Les coordonnées sont dérivées de +l'adresse : + +```text +x = (address & 0xFFF) >> 2 +y = (address >> 12) & 0x3FF +bit 0x00400000 = plan profondeur ; sinon plan couleur +bit 0x00800000 = écriture/lecture Z+couleur combinée, non implémentée +``` + +Une lecture matérielle : + +1. synchronise la file vidéo au moyen d'une requête bloquante ; +2. lit une tuile du cache EFB, avec readback GPU si elle n'est pas fraîche ; +3. reconvertit couleur ou profondeur au format attendu par le jeu ; +4. applique le mode alpha read du registre PE. + +Les pokes matériels sont mis en file et groupés en petits draws, donc ils ne bloquent pas le CPU. +Ils sont flushés avant la prochaine primitive car ils partagent les stream buffers. + +Les accès hors EFB ou désactivés par `bEFBAccessEnable` renvoient zéro/ne font rien. Dans le backend +logiciel actuel, les peeks lisent l'EFB logicielle, mais `PokeColor`, `PokeDepth` et la +réinterprétation de format sont des stubs. + +### 13.3 Clear + +Une copie EFB peut demander un clear après la copie. `BPFunctions::ClearScreen` respecte les bits +d'écriture couleur/alpha/Z, supprime l'alpha des formats qui n'en ont pas et quantifie couleur/Z au +format invité avant d'appeler `FramebufferManager::ClearEFB`. Le scissor et le viewport GX sont +ensuite restaurés. + +## 14. Copies EFB vers texture ou XFB + +### 14.1 Déclenchement BP + +L'écriture `BPMEM_TRIGGER_EFB_COPY` : + +- calcule l'adresse destination `copyTexDest << 5` ; +- calcule le stride `copyDestStride << 5` ; +- construit le rectangle source inclusif à partir de top-left et width/height-minus-one ; +- choisit copie couleur ou profondeur ; +- choisit texture tuilée ou XFB YUYV ; +- applique half scale, Y scale, intensité, gamma, clamps et filtre vertical ; +- déclenche le clear optionnel après la copie. + +Les copies complètement hors EFB sont ignorées. Les copies partiellement hors limites sont clampées. +Le matériel produit vraisemblablement des valeurs dépendant de son adressage interne ; Dolphin ne +reproduit pas ces valeurs indéterminées. + +### 14.2 Trois représentations compatibles + +```mermaid +flowchart TD + EFB["EFB couleur/profondeur"] --> TRIGGER["CopyRenderTargetToTexture"] + TRIGGER --> RAM["Encodage natif dans RAM invitée"] + TRIGGER --> VRAM["Copie convertie dans texture hôte"] + RAM --> CPU["Lecture/modification par le CPU invité"] + RAM --> DEC["Redécodage lors d'un futur Load"] + VRAM --> FAST["Réutilisation rapide et copie upscalée"] + CPU --> HASH["Hash différent"] + HASH --> DEC + VRAM --> HASHOK["Hash RAM identique"] + HASHOK --> FAST +``` + +`CopyRenderTargetToTexture` peut maintenir : + +- une copie RAM exacte au format invité, nécessaire si le CPU la lit ou la modifie ; +- une copie VRAM directement réutilisable, plus rapide et éventuellement upscalée ; +- les deux, avec hash pour détecter si la RAM a divergé. + +Si les copies RAM sont différées, le readback encodé reste dans une staging texture associée à +l'entrée. Il est flushé avant un token, draw done, savestate, invalidation ou toute utilisation qui +doit rendre la RAM observable. Une nouvelle copie couvrant entièrement l'ancienne peut jeter ce +readback en attente. + +Si le jeu relit la destination comme texture et que le hash est inchangé, Dolphin garde la copie +VRAM. S'il a changé, l'entrée devient dynamique et la RAM est redécodée. Un stride trop petit force +le chemin RAM, car seule cette représentation conserve l'image volontairement « brouillée ». + +### 14.3 Spécificités XFB + +Une copie XFB convertit l'EFB en lignes YUYV, applique le Y scale et produit une hauteur de sortie. +Le cache peut conserver la version VRAM tout en écrivant — ou en laissant des valeurs neutralisées +dans — la RAM selon la configuration. + +Des jeux composent une image à partir de plusieurs copies XFB partielles. `StitchXFBCopy` cherche les +copies qui recouvrent l'adresse demandée par la VI, les trie dans l'ordre de création et les assemble +dans une texture conteneur. Si aucune copie VRAM valide n'existe, `GetXFBTexture` décode le YUYV de la +RAM, puis applique les éventuelles copies partielles encore valides. + +L'événement `after_frame_event` est déclenché à chaque copie XFB. C'est le meilleur marqueur de fin +de frame disponible dans le flux, mais pas une vérité absolue : certains jeux font plusieurs copies +par image. + +## 15. Pixel Engine et retour vers le CPU + +Le Pixel Engine possède des registres MMIO CPU à la base physique `0x0C001000`. Ses deux retours +essentiels sont : + +- **token** : valeur 16 bits, avec interruption optionnelle PI `0x200` ; +- **finish/draw done** : interruption PI `0x400` et notification de frame au thread CPU. + +Les commandes BP token/draw done flushent d'abord les copies EFB en attente, les bindings de texture +obsolètes et le cache de peeks. `PixelEngineManager` fusionne les événements en attente sous mutex, +puis planifie `SetTokenFinish_OnMainThread` par `CoreTiming`. + +En Dual Core normal, l'événement est injecté depuis le thread non-CPU avec délai nul, car Dolphin ne +modélise pas le timing GPU avec assez de précision. En Single Core et mode déterministe, un délai +minimal de 500 cycles est imposé pour laisser au jeu le temps d'armer l'interruption. + +Les registres MMIO Z/blend/alpha du PE sont stockés séparément de `bpmem`. Dans cette révision, le mode +alpha read influence les peeks, mais les autres registres de poke ne pilotent pas un pipeline complet +de Z-test/blend pour les writes CPU. + +## 16. Video Interface et présentation + +### 16.1 La VI ne rend pas la 3D + +La VI lit une XFB déjà produite. Elle possède ses propres registres MMIO à `0x0C002000` et progresse +par demi-lignes selon `CoreTiming`. `VideoInterfaceManager::Update` déclenche les interruptions de +raster et les débuts/fins des champs odd/even. + +À `OutputField`, elle calcule : + +```text +fbWidth = WPL × 16 pixels +fbStride = STD × 16 × 2 octets +fbHeight = ACV lignes +xfbAddr = registre top ou bottom selon le champ +``` + +Le mode force-progressive peut réunir les deux champs en divisant le stride et en doublant la +hauteur. L'option early XFB output choisit le début plutôt que la fin de la zone active pour réduire +la latence. + +### 16.2 De la VI au backbuffer + +Sans Immediate XFB : + +1. `Video_OutputXFB` synchronise le FIFO déterministe si nécessaire ; +2. une requête vidéo appelle `Presenter::ViSwap` avec l'instant émulé et l'instant hôte cible ; +3. `FetchXFB` obtient une texture XFB du cache ; +4. les XFB identiques peuvent être reconnues comme frames dupliquées ; +5. `Present` flushe le batch GX, lie et efface le backbuffer ; +6. le post-processeur blitte la XFB avec aspect ratio, crop et stéréo ; +7. l'UI est dessinée, puis `PresentBackbuffer` remet l'image au système de fenêtres. + +Avec Immediate XFB, la copie BP appelle `Presenter::ImmediateSwap` immédiatement, sans attendre le +scanout VI. Cette option réduit la latence mais modifie volontairement le moment de présentation. + +La VI travaille actuellement avec un instantané unique des registres pour tout un champ. Un jeu qui +modifie les registres pendant le scanout ne reçoit donc pas une reconstruction ligne par ligne ; le +réglage early/late choisit seulement quel instantané est le plus acceptable. + +## 17. Backends + +### 17.1 Backends accélérés + +| Backend | Répertoire | Dernière frontière | +|---|---|---| +| D3D11 | [`VideoBackends/D3D`](../Source/Core/VideoBackends/D3D) | `ID3D11DeviceContext::DrawIndexed` | +| D3D12 | [`VideoBackends/D3D12`](../Source/Core/VideoBackends/D3D12) | command list D3D12 | +| Metal | [`VideoBackends/Metal`](../Source/Core/VideoBackends/Metal) | encodeur Metal | +| OpenGL | [`VideoBackends/OGL`](../Source/Core/VideoBackends/OGL) | draw GL | +| Vulkan | [`VideoBackends/Vulkan`](../Source/Core/VideoBackends/Vulkan) | `vkCmdDrawIndexed` | + +Ils partagent le décodage GX, les états CP/XF/BP, la génération de shaders, la gestion EFB et le cache +de textures. Ils diffèrent surtout par les objets API, stream buffers, compilateurs de shaders, +synchronisation de command buffers et capacités déclarées. + +### 17.2 Backend logiciel + +[`VideoBackends/Software`](../Source/Core/VideoBackends/Software) réutilise le même flux jusqu'aux +sommets convertis, puis remplace le draw hôte par : + +```text +SWVertexLoader + -> TransformUnit (matrices, éclairage, texgen) + -> SetupUnit (assemblage) + -> Clipper + -> Rasterizer + -> Tev + TextureSampler + -> SWEfbInterface +``` + +Il est beaucoup plus lent et destiné au débogage. Il donne une seconde implémentation de nombreuses +règles GX, utile pour comparer les résultats, mais il ne constitue pas une référence parfaite : +plusieurs opérations EFB et améliorations ne sont pas implémentées. + +### 17.3 Backend Null + +[`VideoBackends/Null`](../Source/Core/VideoBackends/Null) consomme le flux et maintient la logique +commune sans produire d'image. Il permet d'isoler le coût CPU du frontend et de valider qu'un flux se +décode sans dépendre d'une API graphique. + +## 18. Niveau de fidélité réel + +La table suivante fait partie du contrat de cette documentation. « Émulé » ne signifie pas +nécessairement « cycle exact ». + +| Domaine | État dans cette révision | Conséquence | +|---|---|---| +| Protocole gather/FIFO 32 octets | Émulé structurellement | Pointeurs, wrap, distance et commandes partielles sont conservés. | +| Timing CP/GPU | Approximatif | Coûts fixes par commande ; pas de simulation des files internes ou stalls exacts. | +| WPAR `BNE` | Stub `false` | Évite des hangs, ne reproduit pas les transferts outstanding. | +| Registres clear/métriques CP | Stub ou constantes | Les jeux ne voient pas de compteurs matériels réels. | +| Grammaire principale GX | Émulée | CP/XF/BP, primitives et display lists sont décodées. | +| Opcodes métriques / invalidate VC | Reconnus sans effet complet | Journalisation et timing seulement. | +| VCD/VAT et conversion des sommets | Émulés et testés | Large couverture de formats, chemins générique/x64/ARM64. | +| Incohérences CP/XF/BP | Tolérées autant que possible | Dolphin peut rendre alors que le matériel aurait bloqué. | +| XF error/diag/clock/perf et registres inconnus | Non implémentés | Valeurs stockées/loggées sans pipeline matériel associé. | +| Raster/TEV hardware backend | Traduction fonctionnelle | Dépend des capacités/précisions de l'API et des contournements shader. | +| Raster/TEV software backend | Implémentation CPU séparée | Utile au diagnostic, mais pas complète pour toutes les opérations périphériques. | +| TMEM | Contenu preload/TLUT réel + cache heuristique | Pas d'éviction texel/ligne exacte ni sampler feedback. | +| Invalidation TMEM paramétrée | Invalidation globale | Plus conservateur que le matériel. | +| EFB couleur/profondeur | Représentation hôte améliorée | Formats 6/16 bits et Z16 sont quantifiés logiquement, pas stockés nativement. | +| MSAA 3 échantillons du GPU invité | Non émulé directement | Remplacé par le MSAA hôte configuré et une EFB plus précise. | +| Changement de format EFB | Optionnel | La réinterprétation exacte dépend de `bEFBEmulateFormatChanges`. | +| Accès EFB Z+couleur 64 bits | Non implémenté | Log d'erreur, aucune sémantique complète. | +| Alpha des pokes EFB matériels | Sémantique à confirmer | Le canal est converti et écrit, mais le code signale que sa dépendance au mode PE reste à vérifier. | +| Pokes du backend logiciel | Stubs | Les writes CPU EFB n'y modifient pas l'image. | +| EFB du backend logiciel en RGB565/Z16 et MSAA | Incomplet | Le code marque RGB565/Z16 comme incorrect et le multisampling comme non pris en charge. | +| Copies EFB hors limites | Approximation clamp/ignore | Les valeurs indéterminées du matériel ne sont pas reproduites. | +| Downsample copies à IR > 2× | Filtrage approximatif | Le commentaire du code signale qu'un filtrage plus complexe serait nécessaire. | +| BP field mask / field mode | TODO | La VI et des hacks gèrent l'affichage, mais pas l'écriture EFB par champ exacte. | +| Tokens/finish | Ordre logique émulé, timing approximatif | Délai nul Dual Core ou minimum 500 cycles dans d'autres modes. | +| Timing analogique VI | Approximatif | Les transformateurs de retour horizontal/vertical sont modélisés par une dent de scie idéale. | +| Scanout VI | Champs et demi-lignes | Pas de composition des changements de registres pendant chaque ligne. | +| Backend/API hôte | Couche d'adaptation | Les bugs drivers et capacités modifient la stratégie sans changer l'état invité. | + +### 18.1 Réglages qui changent volontairement le résultat + +Les améliorations et hacks vivent dans [`VideoConfig.h`](../Source/Core/VideoCommon/VideoConfig.h). +Les plus structurants sont : + +- résolution interne, MSAA/SSAA, anisotropie et filtrage forcé ; +- true color, HDR, correction colorimétrique et post-processing ; +- widescreen hack, crop, stéréo et vertex rounding ; +- fast depth, pixel lighting et CPU culling ; +- skip EFB/XFB copy to RAM, scaled EFB copies et deferred copies ; +- désactivation/tiling des EFB accesses, performance queries et bounding box ; +- Immediate XFB et suppression des XFB dupliquées ; +- textures haute résolution et mods graphiques. + +Ces options ne sont pas de simples optimisations invisibles. Certaines améliorent la fidélité +perçue, d'autres échangent explicitement exactitude, latence et performance. + +## 19. Savestates et invariants + +`VideoCommon_DoState` sérialise : + +- `bpmem`, `g_main_cp_state`, `xfmem` et `s_tex_mem` ; +- état heuristique TMEM ; +- tampon FIFO, pointeurs et registres CP ; +- PE, managers de constantes et `VertexManager` ; +- contenu EFB, texture cache, Presenter, bounding box et widescreen. + +Le GPU est synchronisé autour de la sauvegarde ; `g_preprocess_cp_state` n'est donc pas stocké +séparément et est recopié depuis l'état principal au chargement. `BPReload` reconstruit les effets de +bord backend et tous les chargeurs de sommets sont marqués sales. + +Invariants importants vérifiés par assertions ou logs : + +- FIFO lié : base/end/write pointer PI et CP identiques ; +- distance FIFO jamais négative et jamais supérieure à la capacité ; +- commande primitive : octets consommés = `vertex_size × count` ; +- format CP compatible avec le vertex spec XF ; +- nombre de texgens/canaux XF compatible avec BP avant un draw ; +- pipeline GX flushé avant toute utility draw qui réutilise ses buffers. + +## 20. Validation existante + +### 20.1 Tests unitaires + +[`VertexLoaderTest.cpp`](../Source/UnitTests/VideoCommon/VertexLoaderTest.cpp) est actuellement le seul +fichier de tests sous `Source/UnitTests/VideoCommon`. Il couvre notamment : + +- unicité des `VertexLoaderUID` ; +- positions directes et indexées dans de nombreux formats ; +- couleurs, normales, tangentes/binormales et texcoords ; +- composantes absentes ou sautées ; +- fractions, conversions, endianness et chemins de chargeur disponibles. + +Cette couverture solide du décodage de sommets ne constitue pas une couverture automatique du FIFO, +des registres, du TEV, des copies ou de la VI. + +### 20.2 FIFO recorder/player + +[`Core/FifoPlayer`](../Source/Core/Core/FifoPlayer) enregistre les commandes GP et les plages mémoire +qu'elles consultent : arrays, indexed XF, display lists, textures, TMEM et destinations générées. Le +player reconstruit les registres CP et rejoue le FIFO sans exécuter le jeu. + +C'est l'outil d'intégration central pour : + +- reproduire une frame GPU ; +- comparer deux backends ; +- inspecter l'état CP/XF/BP à une commande ; +- détecter une régression de shader, texture ou EFB ; +- isoler un problème graphique d'un problème CPU/timing du jeu. + +### 20.3 Diagnostics d'exécution + +Le code possède aussi : + +- logs nommés CP/BP/XF/PE/VI ; +- alertes d'opcode inconnu avec état complet du FIFO ; +- analytics de quirks pour commandes ou formats atypiques ; +- statistiques par frame sur loads, primitives, draws, copies, peeks et tokens ; +- backend Software et backend Null comme chemins de comparaison. + +## 21. Lacunes de validation à combler + +Pour transformer cette carte en spécification régressive complète, les tests prioritaires sont : + +1. **Opcode decoder** : taille partielle/complète, endianness, alignement display list et chaque + callback. +2. **FIFO circulaire** : wrap inclusif, linked/unlinked, watermarks, safe read pointer et breakpoint. +3. **Modes de threads** : même ordre de callbacks entre Single Core, Dual Core et déterministe. +4. **BP mask/side effects** : écriture identique, masque one-shot, token, finish, TLUT et copy. +5. **XF indexed loads** : snapshot déterministe et invalidation précise des constantes. +6. **Primitive/index generation** : toutes les topologies avec et sans primitive restart. +7. **Shader golden tests** : UID et source produits pour des états CP/XF/BP représentatifs. +8. **Texture/TMEM** : chevauchements even/odd, palettes, EFB copy modifiée par le CPU et invalidation. +9. **EFB/XFB** : formats, strides, copies partielles, stitching, clear et round-trip RAM. +10. **PE/VI** : ordre interrupt/token/finish et présentation odd/even/duplicate/immediate. + +Les comportements marqués heuristiques ou inconnus exigent en plus des tests sur console réelle ; un +test Dolphin ne peut pas, à lui seul, établir la vérité matérielle. + +## 22. Chemins d'appel de référence + +### 22.1 Une primitive normale + +```text +PPC store + -> MMU::WriteToHardware + -> GPFifoManager::Write* / UpdateGatherPipe + -> CommandProcessorManager::GatherPipeBursted + -> FifoManager::RunGpuLoop ou RunGpuOnCpu + -> OpcodeDecoder::RunFifo + -> RunCallback::OnPrimitiveCommand + -> VertexLoaderManager::RunVertices + -> VertexManagerBase::PrepareForAdditionalData / AddIndices / FlushData + -> VertexManagerBase::Flush + -> ShaderCache + TextureCacheBase + -> VertexManagerBase::RenderDrawCall + -> AbstractGfx::DrawIndexed + -> EFB +``` + +### 22.2 Une écriture de registre BP + +```text +Opcode 0x61 + -> RunCallback::OnBP + -> LoadBPReg + -> application de bpmem.bpMask + -> BPWritten + -> FlushPipeline + -> mise à jour bpmem + -> dirty flag / état hôte / copie / token selon l'adresse +``` + +### 22.3 Une copie puis présentation + +```text +BPMEM_TRIGGER_EFB_COPY + -> TextureCacheBase::CopyRenderTargetToTexture + -> RAM invitée et/ou texture XFB en VRAM + -> VideoEvents::after_frame_event + -> VideoInterfaceManager::OutputField + -> VideoBackendBase::Video_OutputXFB + -> Presenter::ViSwap + -> TextureCacheBase::GetXFBTexture + -> Presenter::Present + -> AbstractGfx::PresentBackbuffer +``` + +### 22.4 Un peek EFB + +```text +PPC load dans la plage EFB + -> MMU::EFB_Read + -> EFBInterfaceBase::PeekColor/PeekDepth + -> AsyncRequests::PushBlockingEvent + -> VertexManagerBase::Flush + -> FramebufferManager::PeekEFB* + -> cache/readback du GPU hôte + -> conversion au format GX + alpha read PE + -> valeur PPC +``` + +## 23. Guide de diagnostic + +| Symptôme | Première zone à vérifier | Questions utiles | +|---|---|---| +| Opcode inconnu / FIFO corrompu | Gather, CP, `Fifo`, `OpcodeDecoding` | La distance et les pointeurs sont-ils cohérents ? Une commande partielle a-t-elle été consommée ? Le mode Dual Core change-t-il le bug ? | +| Géométrie déformée | CP, `VertexLoader`, XF | VCD/VAT, bases/strides, endianness, matrix index et vertex spec correspondent-ils ? | +| Primitive manquante | `IndexGenerator`, culling, pipeline async | Topologie/primitive restart corrects ? Cull all ? Shader spécialisé encore absent en skip mode ? | +| Couleur/alpha/fog faux | BP, `PixelShaderGen`, constantes | Étages TEV, swizzles, K colors, alpha test, dst alpha et quantification EFB corrects ? | +| Texture ancienne ou scintillante | `TextureCacheBase`, TMEM | Invalidation, hash, overlap, palette, preload et EFB copy dynamique corrects ? | +| Effet écran/miroir faux | Copie EFB | Format, stride, y scale, half scale, RAM vs VRAM et clear corrects ? | +| Image 3D correcte mais affichage faux | XFB, VI, `Presenter` | Adresse top/bottom, WPL/STD/ACV, interlace, stitching et aspect ratio corrects ? | +| Jeu bloqué en attente GPU | CP/PE interrupts et synchronisation | GPRead, breakpoint, watermark, token/finish enable et événement CPU sont-ils dans le bon ordre ? | +| Bug uniquement backend matériel | Shader UID, `RenderState`, capacités backend | Le backend Software reproduit-il le bug ? Une capacité ou un workaround driver change-t-il le pipeline ? | +| Bug uniquement à haute résolution | EFB scale, copies, viewport/scissor | Coordonnées natives et scalées sont-elles mélangées ? Le downsample/copy filter est-il exact ? | + +## 24. Modèle mental final + +Pour raisonner correctement sur un bug GPU Dolphin, suivre quatre états dans l'ordre : + +1. **Flux** : quels octets le PPC a-t-il réellement placés dans le FIFO, et quand sont-ils devenus + visibles au CP ? +2. **Machine GX** : quel état CP/XF/BP existe exactement au début de la primitive ? +3. **Traduction** : quels sommets, textures, constantes, shaders et états hôte cet instantané a-t-il + produits ? +4. **Sortie** : comment le résultat EFB a-t-il été copié en XFB, scanné par la VI et présenté ? + +Une anomalie visible peut provenir de chacune de ces couches. L'erreur classique consiste à +inspecter le shader final alors que le vrai défaut est un read pointer FIFO, ou à inspecter le +rasteriseur alors que l'image EFB correcte est ensuite mal assemblée dans la XFB. diff --git a/docs/Wii_IOS_LLE.md b/docs/Wii_IOS_LLE.md new file mode 100644 index 0000000000..20cc34fb5e --- /dev/null +++ b/docs/Wii_IOS_LLE.md @@ -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.