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.