IOS: checkpoint native Wii Shop connectivity and Starlet optimizations

Add opt-in AX88772 Ethernet with libslirp NAT, pinned Windows runtime setup and USB/network regressions. Correct Hollywood DI/reset interrupt routing and physical SRAM DMA for AES, SHA, NAND, SDIO and OHCI. Keep aligned Thumb bus accesses inside native JIT blocks.

Validated: 164 targeted tests pass. User confirmed Wii Shop connection and channel-list navigation at 100% speed / 59.96 FPS on 2026-09-12. Downloads and general channel performance remain unvalidated; local firmware, keys and runtime data are excluded.
This commit is contained in:
2026-09-12 11:08:25 +02:00
parent ebb753d09a
commit b931671fde
24 changed files with 2952 additions and 64 deletions
+6
View File
@@ -404,8 +404,12 @@ add_library(core
IOS/MIOS.h
IOS/Starlet/ARMCore.cpp
IOS/Starlet/ARMCore.h
IOS/Starlet/AX88772.cpp
IOS/Starlet/AX88772.h
IOS/Starlet/NANDJournal.cpp
IOS/Starlet/NANDJournal.h
IOS/Starlet/SlirpNetwork.cpp
IOS/Starlet/SlirpNetwork.h
IOS/Starlet/Starlet.cpp
IOS/Starlet/Starlet.h
IOS/Starlet/StarletMemory.cpp
@@ -859,6 +863,8 @@ if(MSVC)
target_link_libraries(core PRIVATE use_pch)
endif()
target_include_directories(core PRIVATE ${PROJECT_SOURCE_DIR}/Externals/libslirp/include)
if(USE_RETRO_ACHIEVEMENTS)
target_link_libraries(core PUBLIC rcheevos)
target_compile_definitions(core PUBLIC -DUSE_RETRO_ACHIEVEMENTS)
+1
View File
@@ -258,6 +258,7 @@ const Info<std::string> MAIN_GPU_DETERMINISM_MODE{{System::Main, "Core", "GPUDet
const Info<s32> MAIN_OVERRIDE_BOOT_IOS{{System::Main, "Core", "OverrideBootIOS"}, -1};
const Info<bool> MAIN_WII_IOS_LLE{{System::Main, "Core", "WiiIOSLLE"}, false};
const Info<bool> MAIN_WII_STARLET_JIT{{System::Main, "Core", "WiiStarletJIT"}, true};
const Info<bool> MAIN_WII_LLE_ETHERNET{{System::Main, "Core", "WiiLLEEthernet"}, false};
GPUDeterminismMode GetGPUDeterminismMode()
{
+1
View File
@@ -168,6 +168,7 @@ extern const Info<bool> MAIN_REAL_WII_REMOTE_REPEAT_REPORTS;
extern const Info<s32> MAIN_OVERRIDE_BOOT_IOS;
extern const Info<bool> MAIN_WII_IOS_LLE;
extern const Info<bool> MAIN_WII_STARLET_JIT;
extern const Info<bool> MAIN_WII_LLE_ETHERNET;
extern const Info<std::string> MAIN_WII_NUS_SHOP_URL;
extern const Info<bool> MAIN_WII_WIILINK_ENABLE;
+4 -2
View File
@@ -33,12 +33,14 @@ 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,
INT_CAUSE_RST_BUTTON = 0x40000,
// Hollywood IRQ17/18, distinct from the Broadway PI interrupt numbers.
// https://wiibrew.org/wiki/Hollywood/IRQs
INT_CAUSE_RST_BUTTON = 0x20000,
INT_CAUSE_DI = 0x40000,
INT_CAUSE_IPC_BROADWAY = 0x40000000,
INT_CAUSE_IPC_STARLET = 0x80000000
+64 -19
View File
@@ -76,10 +76,10 @@ ARMJitX64::ARMJitX64(ARMCore& core) : m_core(core)
m_executed_instructions_offset =
static_cast<s32>(reinterpret_cast<const u8*>(&m_core.m_executed_instructions) - base);
m_control_offset = static_cast<s32>(reinterpret_cast<const u8*>(&m_core.m_cp15.control) - base);
m_translation_table_base_offset = static_cast<s32>(
reinterpret_cast<const u8*>(&m_core.m_cp15.translation_table_base) - base);
m_domain_access_control_offset = static_cast<s32>(
reinterpret_cast<const u8*>(&m_core.m_cp15.domain_access_control) - base);
m_translation_table_base_offset =
static_cast<s32>(reinterpret_cast<const u8*>(&m_core.m_cp15.translation_table_base) - base);
m_domain_access_control_offset =
static_cast<s32>(reinterpret_cast<const u8*>(&m_core.m_cp15.domain_access_control) - base);
m_process_id_offset =
static_cast<s32>(reinterpret_cast<const u8*>(&m_core.m_cp15.process_id) - base);
m_tlb_generation_offset =
@@ -240,15 +240,13 @@ void ARMJitX64::GenerateDispatcher()
SetJumpTarget(no_mmu_way1_key_miss);
CMP(32, MDisp(R11, static_cast<s32>(2 * sizeof(FastEntry) + offsetof(FastEntry, key))), R(EAX));
FixupBranch no_mmu_way2_key_miss = J_CC(CC_NE, Jump::Near);
CMP(32, MDisp(R11,
static_cast<s32>(2 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))),
CMP(32, MDisp(R11, static_cast<s32>(2 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))),
R(R8));
FixupBranch no_mmu_hit_way2 = J_CC(CC_E, Jump::Near);
SetJumpTarget(no_mmu_way2_key_miss);
CMP(32, MDisp(R11, static_cast<s32>(3 * sizeof(FastEntry) + offsetof(FastEntry, key))), R(EAX));
FixupBranch no_mmu_cache_miss_key = J_CC(CC_NE, Jump::Near);
CMP(32, MDisp(R11,
static_cast<s32>(3 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))),
CMP(32, MDisp(R11, static_cast<s32>(3 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))),
R(R8));
FixupBranch no_mmu_cache_miss_page = J_CC(CC_NE, Jump::Near);
ADD(64, R(R11), Imm8(static_cast<u8>(3 * sizeof(FastEntry))));
@@ -331,15 +329,13 @@ void ARMJitX64::GenerateDispatcher()
SetJumpTarget(mmu_way1_key_miss);
CMP(32, MDisp(R11, static_cast<s32>(2 * sizeof(FastEntry) + offsetof(FastEntry, key))), R(EAX));
FixupBranch mmu_way2_key_miss = J_CC(CC_NE, Jump::Near);
CMP(32, MDisp(R11,
static_cast<s32>(2 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))),
CMP(32, MDisp(R11, static_cast<s32>(2 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))),
R(R8));
FixupBranch mmu_hit_way2 = J_CC(CC_E, Jump::Near);
SetJumpTarget(mmu_way2_key_miss);
CMP(32, MDisp(R11, static_cast<s32>(3 * sizeof(FastEntry) + offsetof(FastEntry, key))), R(EAX));
FixupBranch mmu_cache_miss_key = J_CC(CC_NE, Jump::Near);
CMP(32, MDisp(R11,
static_cast<s32>(3 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))),
CMP(32, MDisp(R11, static_cast<s32>(3 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))),
R(R8));
FixupBranch mmu_cache_miss_page = J_CC(CC_NE, Jump::Near);
ADD(64, R(R11), Imm8(static_cast<u8>(3 * sizeof(FastEntry))));
@@ -1340,8 +1336,8 @@ bool ARMJitX64::CanEmitARMMemory(u32 instruction) const
const bool direct_pc_load = load && rd == 15 && (instruction >> 28) == 0xe && preindex &&
!writeback && (instruction & (1U << 22)) == 0;
return (rd != 15 || direct_pc_load) && !(rn == 15 && (!preindex || writeback)) &&
!(load && writeback && rn == rd) &&
!(register_offset && (instruction & (1U << 4)) != 0) && !(register_offset && rm == 15);
!(load && writeback && rn == rd) && !(register_offset && (instruction & (1U << 4)) != 0) &&
!(register_offset && rm == 15);
}
bool ARMJitX64::EmitARMHalfwordMemory(u32 instruction, u32 address)
@@ -2177,7 +2173,8 @@ bool ARMJitX64::EmitThumbMemory(u16 instruction, u32 address)
}
const FixupBranch direct_done = J();
EmitThumbMemorySlowPath(slow_paths, instruction, address, direct_done);
EmitThumbMemorySlowPath(slow_paths, instruction, address, direct_done, access_size, rd, load,
sign_extend);
return true;
}
@@ -2388,12 +2385,49 @@ void ARMJitX64::EmitFastmemAddress(std::vector<FixupBranch>* slow_paths, u32 acc
}
void ARMJitX64::EmitThumbMemorySlowPath(const std::vector<FixupBranch>& slow_paths, u16 instruction,
u32 address, FixupBranch direct_done)
u32 address, FixupBranch direct_done, u32 access_size,
u32 rd, bool load, bool sign_extend)
{
for (const FixupBranch& slow_path : slow_paths)
SetJumpTarget(slow_path);
EmitFallbackThumb(instruction, address);
EmitBlockExit(m_compile_instruction_count, m_compile_native_instruction_count);
// IOS Thumb code repeatedly accesses SRAM and MMIO. These instructions are already decoded:
// perform the exact bus transaction without re-entering ExecuteThumb and the block dispatcher.
// Do not broaden SRAM fastmem: boot0 protection, split-window holes, and device side effects
// remain the bus's responsibility. Keep unaligned accesses on the interpreter path, including
// register-offset word rotation and halfwords spanning two virtual translation granules.
FixupBranch unaligned;
if (access_size > 1)
{
TEST(32, R(R9), Imm32(access_size - 1));
unaligned = J_CC(CC_NZ, Jump::Near);
}
MOV(32, R(ABI_PARAM2), R(R9));
if (load)
{
MOV(32, R(ABI_PARAM4), Imm32(0));
MOV(32, R(ABI_PARAM3), Imm32(access_size));
MOV(64, R(ABI_PARAM1), ImmPtr(this));
ABI_CallFunction(ReadMemorySlow);
if (sign_extend)
MOVSX(32, access_size * 8, EAX, R(EAX));
MOV(32, MStoredRegister(rd), R(EAX));
}
else
{
MOV(32, R(ABI_PARAM4), MStoredRegister(rd));
MOV(32, R(ABI_PARAM3), Imm32(access_size));
MOV(64, R(ABI_PARAM1), ImmPtr(this));
ABI_CallFunction(WriteMemorySlow);
}
if (access_size > 1)
{
const FixupBranch bus_done = J(Jump::Near);
SetJumpTarget(unaligned);
EmitFallbackThumb(instruction, address);
EmitBlockExit(m_compile_instruction_count, m_compile_native_instruction_count);
SetJumpTarget(bus_done);
}
SetJumpTarget(direct_done);
LoadRegisterCache();
}
@@ -2926,7 +2960,7 @@ void ARMJitX64::ExceptionReturn(ARMJitX64* jit, u32 target)
}
u32 ARMJitX64::ReadMemorySlow(ARMJitX64* jit, u32 physical_address, u32 access_size,
u32 byte_offset)
u32 byte_offset)
{
++jit->m_slow_read_count;
if ((jit->m_slow_read_count & 0xff) == 0)
@@ -2952,6 +2986,11 @@ u32 ARMJitX64::ReadMemorySlow(ARMJitX64* jit, u32 physical_address, u32 access_s
ARMCore& core = jit->m_core;
if (access_size == 1)
return core.m_bus.Read8(physical_address);
if (access_size == 2)
{
const u16 value = core.m_bus.Read16(physical_address);
return core.m_big_endian ? value : std::byteswap(value);
}
u32 value = core.m_bus.Read32(physical_address & ~3U);
if (!core.m_big_endian)
@@ -2988,6 +3027,12 @@ void ARMJitX64::WriteMemorySlow(ARMJitX64* jit, u32 physical_address, u32 access
core.m_bus.Write8(physical_address, static_cast<u8>(value));
return;
}
if (access_size == 2)
{
const u16 halfword = static_cast<u16>(value);
core.m_bus.Write16(physical_address, core.m_big_endian ? halfword : std::byteswap(halfword));
return;
}
if (!core.m_big_endian)
value = std::byteswap(value);
+6 -8
View File
@@ -69,10 +69,7 @@ public:
{
return m_dispatch_key_miss_with_empty_slot_count;
}
u64 GetDispatchKeyMissWithFullSetCount() const
{
return m_dispatch_key_miss_with_full_set_count;
}
u64 GetDispatchKeyMissWithFullSetCount() const { return m_dispatch_key_miss_with_full_set_count; }
u64 GetDispatchKeyMissWithStaleTranslationCount() const
{
return m_dispatch_key_miss_with_stale_translation_count;
@@ -171,9 +168,9 @@ private:
static size_t GetFastEntrySetIndex(u32 key);
static const u8* Dispatch(ARMJitX64* jit, DispatchReason reason, u32 generated_key,
u32 generated_set_offset);
Block* GetOrCompileBlock(u32 address, u32* physical_address_out,
const u8** first_descriptor_out, u32* first_descriptor_value_out,
const u8** second_descriptor_out, u32* second_descriptor_value_out);
Block* GetOrCompileBlock(u32 address, u32* physical_address_out, const u8** first_descriptor_out,
u32* first_descriptor_value_out, const u8** second_descriptor_out,
u32* second_descriptor_value_out);
Block CompileBlock(u32 address, bool thumb);
bool EmitDirectARM(u32 instruction, u32 address, bool* terminal, bool* dispatcher_exit);
bool EmitARMMultiplyLong(u32 instruction);
@@ -193,7 +190,8 @@ private:
SRAMFastmemAccess sram_access = SRAMFastmemAccess::None,
bool arm_unaligned_word = false);
void EmitThumbMemorySlowPath(const std::vector<Gen::FixupBranch>& slow_paths, u16 instruction,
u32 address, Gen::FixupBranch direct_done);
u32 address, Gen::FixupBranch direct_done, u32 access_size, u32 rd,
bool load, bool sign_extend);
void EmitARMMemorySlowPath(const std::vector<Gen::FixupBranch>& slow_paths, u32 instruction,
u32 address, Gen::FixupBranch direct_done);
void EmitThumbAddSub(u16 instruction);
+579
View File
@@ -0,0 +1,579 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/IOS/Starlet/AX88772.h"
#include <algorithm>
#include <string_view>
#include <utility>
#include <fmt/ranges.h>
#include "Common/ChunkFile.h"
#include "Common/Logging/Log.h"
namespace IOS::LLE
{
namespace
{
u16 LE16(std::span<const u8> bytes, size_t offset = 0)
{
return u16(bytes[offset]) | (u16(bytes[offset + 1]) << 8);
}
std::vector<u8> Word(u16 value)
{
return {static_cast<u8>(value), static_cast<u8>(value >> 8)};
}
constexpr size_t MAX_FRAME = 1518;
constexpr size_t MAX_QUEUE = 128;
} // namespace
AX88772::AX88772(Transmit transmit) : m_transmit(std::move(transmit))
{
m_eeprom.fill(0xffff);
for (size_t i = 0; i < 3; ++i)
m_eeprom[4 + i] = LE16(m_mac, i * 2);
// Datasheet section 4.1.6: primary embedded PHY 0x10; no secondary PHY (0xe0).
// IOS also reads this EEPROM word directly, not only USB command 0x19.
m_eeprom[0x11] = 0xe010;
m_eeprom[0x0f] = 0x0105; // Interrupt reports carry BMSR and link partner abilities.
m_eeprom[0x10] = MAX_FRAME;
Reset();
}
void AX88772::Reset()
{
m_address = m_configuration = 0;
m_control_valid = m_control_stalled = false;
m_control.clear();
m_out.clear();
m_tx.clear();
m_rx.clear();
m_rx_offset = m_control_offset = 0;
m_phy.fill(0);
m_phy[0] = 0x3100; // Autonegotiation, 100 Mbps, full duplex.
m_phy[2] = 0x003b;
m_phy[3] = 0x1861; // AX88772A internal PHY, as identified by Linux asix.
m_phy[4] = 0x01e1;
m_phy[5] = 0x45e1;
m_phy[6] = 1;
m_rx_control = m_medium = m_gpio = 0;
m_monitor = 0;
m_ipg = {0x15, 0x0c, 0x12};
m_multicast.fill(0);
m_phy_select = 1;
m_software_reset = 0x28;
m_software_mii = m_eeprom_writable = false;
m_link_event = true;
m_status_frames_remaining = 0;
m_control_log_count = 0;
m_interrupt_reports = 0;
}
bool AX88772::LinkUp() const
{
return m_connected && (m_phy[0] & 0x0800) == 0 && (m_software_reset & 0x40) == 0;
}
void AX88772::DoState(PointerWrap& p)
{
p.Do(m_mac);
p.Do(m_eeprom);
p.Do(m_phy);
p.Do(m_multicast);
p.Do(m_ipg);
p.Do(m_setup);
p.Do(m_control);
p.Do(m_out);
p.Do(m_tx);
p.Do(m_rx);
p.Do(m_control_offset);
p.Do(m_rx_offset);
p.Do(m_rx_control);
p.Do(m_medium);
p.Do(m_gpio);
p.Do(m_monitor);
p.Do(m_phy_select);
p.Do(m_software_reset);
p.Do(m_address);
p.Do(m_configuration);
p.Do(m_control_valid);
p.Do(m_control_stalled);
p.Do(m_software_mii);
p.Do(m_eeprom_writable);
p.Do(m_connected);
p.Do(m_link_event);
p.Do(m_status_frames_remaining);
if (p.IsReadMode() &&
(m_control_offset > m_control.size() ||
(!m_rx.empty() && m_rx_offset > m_rx.front().size()) || m_tx.size() > 65536 ||
m_rx.size() > MAX_QUEUE || m_status_frames_remaining > INTERRUPT_INTERVAL_MS))
p.SetVerifyMode();
}
void AX88772::SetLink(bool connected)
{
if (m_connected != connected)
m_link_event = true;
m_connected = connected;
}
void AX88772::AdvanceUSBFrame()
{
// The status endpoint supplies periodic PHY snapshots, not just link-change
// events (AX88772 datasheet 6.2.1.7). IOS reads it synchronously to check an
// unchanged link too. Keep a single pending snapshot: USB scheduling delays
// must not build up a queue of stale reports or produce a catch-up burst.
if (m_configuration != 0 && m_status_frames_remaining != 0 && --m_status_frames_remaining == 0)
m_link_event = true;
}
u16 AX88772::ReadPHY(u16 reg) const
{
if (reg >= m_phy.size())
return 0xffff;
if (reg == 1)
return 0x7809 | (LinkUp() ? 0x0024 : 0); // Capabilities, link and autoneg complete.
return m_phy[reg];
}
std::vector<u8> AX88772::Descriptor(u16 value) const
{
switch (value >> 8)
{
case 1:
return {18, 1, 0, 2, 0xff, 0xff, 0, 64, 0x95, 0x0b, 0x20, 0x77, 1, 0, 1, 2, 3, 1};
case 2:
// Interrupt IN 1, bulk IN 2, bulk OUT 3. Full-speed bulk packets are 64 bytes.
return {9, 2, 39, 0, 1,
1, 0, 0x80, 125, 9,
4, 0, 0, 3, 0xff,
0xff, 0, 0, 7, 5,
0x81, 3, 8, 0, INTERRUPT_INTERVAL_MS,
7, 5, 0x82, 2, 64,
0, 0, 7, 5, 3,
2, 64, 0, 0};
case 3:
{
if ((value & 0xff) == 0)
return {4, 3, 9, 4};
constexpr std::array<std::string_view, 3> strings{"ASIX", "AX88772A USB Ethernet",
"DolphinLLE0001"};
const u8 index = value & 0xff;
if (index > strings.size())
return {};
const auto str = strings[index - 1];
std::vector<u8> descriptor{static_cast<u8>(2 + str.size() * 2), 3};
for (char c : str)
{
descriptor.push_back(static_cast<u8>(c));
descriptor.push_back(0);
}
return descriptor;
}
default:
return {};
}
}
bool AX88772::PrepareControl()
{
const u8 type = m_setup[0], request = m_setup[1];
const u16 value = LE16(m_setup, 2), index = LE16(m_setup, 4);
const u16 length = LE16(m_setup, 6);
m_control.clear();
m_out.clear();
m_control_offset = 0;
if ((type & 0x60) == 0)
{
if ((type & 0x80) != 0)
{
switch (request)
{
case 0:
m_control = {0, 0};
break;
case 6:
m_control = Descriptor(value);
if (m_control.empty())
return false;
break;
case 8:
m_control = {m_configuration};
break;
case 10:
m_control = {0};
break;
default:
return false;
}
}
else if (length != 0 ||
(request != 1 && request != 3 && request != 5 && request != 9 && request != 11))
return false;
}
else if ((type & 0x60) == 0x40)
{
// Register protocol documented by the Linux drivers/net/usb/asix sources.
if ((type & 0x80) != 0)
{
switch (request)
{
case 0x07:
m_control = Word((value & 0x1f) == 0x10 ? ReadPHY(index & 0x1f) : 0xffff);
break;
case 0x09:
m_control = {static_cast<u8>(m_software_mii ? 1 : 0)};
break;
case 0x0b:
if (value >= m_eeprom.size())
return false;
m_control = Word(m_eeprom[value]);
INFO_LOG_FMT(IOS_USB, "AX88772 EEPROM read word {:#04x} = {:#06x}", value, m_eeprom[value]);
break;
case 0x0f:
m_control = Word(m_rx_control);
break;
case 0x11:
m_control.assign(m_ipg.begin(), m_ipg.end());
break;
case 0x13:
m_control.assign(m_mac.begin(), m_mac.end());
break;
case 0x15:
m_control.assign(m_multicast.begin(), m_multicast.end());
break;
case 0x19:
m_control = {static_cast<u8>(m_eeprom[0x11] >> 8), static_cast<u8>(m_eeprom[0x11])};
break;
case 0x1a:
m_control = Word(m_medium);
break;
case 0x1c:
m_control = {m_monitor};
break;
case 0x1e:
m_control = Word(m_gpio);
break;
case 0x21:
m_control = {0x10};
break; // AX88772A chip code, internal PHY.
default:
return false;
}
}
else
{
switch (request)
{
case 0x06:
case 0x0a:
case 0x0d:
case 0x0e:
case 0x10:
case 0x12:
case 0x1b:
case 0x1d:
case 0x1f:
case 0x20:
case 0x22:
if (length != 0)
return false;
break;
case 0x08:
if (length != 2)
return false;
break;
case 0x0c:
if (length != 0 || value >= 256 || !m_eeprom_writable)
return false;
break;
case 0x14:
if (length != 6)
return false;
break;
case 0x16:
if (length != 8)
return false;
break;
default:
return false;
}
}
}
else
return false;
if (m_control.size() > length)
m_control.resize(length);
return true;
}
bool AX88772::ApplyControl(std::span<const u8> data)
{
const u8 type = m_setup[0], request = m_setup[1];
const u16 value = LE16(m_setup, 2), index = LE16(m_setup, 4);
if ((type & 0x80) != 0)
return true;
if ((type & 0x60) == 0)
{
switch (request)
{
case 1:
return value == 0; // CLEAR_FEATURE(ENDPOINT_HALT).
case 3:
return false; // Unsupported features must not silently succeed.
case 5:
if (value > 127)
return false;
m_address = static_cast<u8>(value);
INFO_LOG_FMT(IOS_USB, "AX88772 assigned USB address {}", value);
return true;
case 9:
if (value > 1)
return false;
m_configuration = static_cast<u8>(value);
INFO_LOG_FMT(IOS_USB, "AX88772 USB configuration {}", value);
m_link_event = true;
return true;
case 11:
return value == 0 && index == 0;
default:
return false;
}
}
switch (request)
{
case 0x06:
m_software_mii = true;
break;
case 0x08:
// PHY address/register are five-bit MDIO fields. An absent PHY does not
// make a valid USB vendor request stall; it simply cannot latch the write.
INFO_LOG_FMT(IOS_USB, "AX88772 MDIO write PHY={:#04x} reg={:#04x} value={:#06x}", value, index,
LE16(data));
if ((value & 0x1f) != 0x10)
break;
m_phy[index & 0x1f] = LE16(data);
// Autonegotiation restart and PHY reset commands self-clear.
if ((index & 0x1f) == 0)
{
m_phy[0] &= ~0x8200;
m_link_event = true;
}
break;
case 0x0a:
m_software_mii = false;
break;
case 0x0c:
m_eeprom[value] = index;
break;
case 0x0d:
m_eeprom_writable = true;
break;
case 0x0e:
m_eeprom_writable = false;
break;
case 0x10:
if (m_rx_control != value)
INFO_LOG_FMT(IOS_USB, "AX88772 RX control = {:#06x}", value);
m_rx_control = value;
break;
case 0x12:
m_ipg = {static_cast<u8>(value), static_cast<u8>(value >> 8), static_cast<u8>(index)};
break;
case 0x14:
std::copy(data.begin(), data.end(), m_mac.begin());
break;
case 0x16:
std::copy(data.begin(), data.end(), m_multicast.begin());
break;
case 0x1b:
if (m_medium != value)
INFO_LOG_FMT(IOS_USB, "AX88772 medium = {:#06x}, link {}", value, LinkUp());
m_medium = value;
break;
case 0x1d:
m_monitor = static_cast<u8>(value);
break;
case 0x1f:
m_gpio = value;
break;
case 0x20:
m_software_reset = static_cast<u8>(value);
if (value & 1)
{
m_rx.clear();
m_rx_offset = 0;
}
if (value & 2)
m_tx.clear();
m_link_event = true;
break;
case 0x22:
m_phy_select = static_cast<u8>(value);
break;
default:
return false;
}
return true;
}
AX88772::Result AX88772::BulkOut(std::span<const u8> data)
{
// A frame may span TDs. AX framing is length + one's complement, little-endian.
if (m_tx.size() + data.size() > 65536)
{
m_tx.clear();
return Result::Stalled;
}
m_tx.insert(m_tx.end(), data.begin(), data.end());
size_t consumed = 0;
while (m_tx.size() - consumed >= 4)
{
const u16 length = LE16(m_tx, consumed);
if (static_cast<u16>(length ^ LE16(m_tx, consumed + 2)) != 0xffff || length > MAX_FRAME)
{
m_tx.clear();
return Result::Stalled;
}
const size_t record_size = 4 + length;
if (m_tx.size() - consumed < record_size)
break;
if (length >= 14 && LinkUp() && m_transmit)
m_transmit(std::span(m_tx).subspan(consumed + 4, length));
consumed += record_size;
}
m_tx.erase(m_tx.begin(), m_tx.begin() + consumed);
return Result::Completed;
}
void AX88772::ReceiveFrame(std::span<const u8> frame)
{
if (!LinkUp() || (m_rx_control & 0x80) == 0 || (m_medium & 0x100) == 0 || frame.size() < 14 ||
frame.size() > MAX_FRAME || m_rx.size() >= MAX_QUEUE)
return;
const bool broadcast =
std::all_of(frame.begin(), frame.begin() + 6, [](u8 b) { return b == 0xff; });
const bool multicast = (frame[0] & 1) != 0;
if ((m_rx_control & 1) == 0 && !std::equal(m_mac.begin(), m_mac.end(), frame.begin()) &&
!(broadcast && (m_rx_control & 8)) && !(multicast && !broadcast && (m_rx_control & 0x12)))
return;
const u16 length = static_cast<u16>(frame.size());
auto packet = Word(length);
auto inverse = Word(static_cast<u16>(~length));
packet.insert(packet.end(), inverse.begin(), inverse.end());
packet.insert(packet.end(), frame.begin(), frame.end());
if (length & 1)
packet.push_back(0);
m_rx.push_back(std::move(packet));
}
AX88772::Result AX88772::Transfer(u8 endpoint, u8 direction, std::span<u8> buffer, size_t* actual)
{
*actual = 0;
if (direction > 2)
return Result::Stalled;
if (endpoint == 0)
{
if (direction == 0)
{
if (buffer.size() != 8)
return Result::Stalled;
std::copy(buffer.begin(), buffer.end(), m_setup.begin());
m_control_valid = true;
m_control_stalled = !PrepareControl();
if (m_control_log_count++ < 128)
INFO_LOG_FMT(IOS_USB,
"AX88772 control {:#04x}/{:#04x} value={:#06x} index={:#06x} "
"length={} response={:02x}",
m_setup[0], m_setup[1], LE16(m_setup, 2), LE16(m_setup, 4), LE16(m_setup, 6),
fmt::join(m_control, " "));
if (m_control_stalled)
WARN_LOG_FMT(
IOS_USB,
"AX88772 unsupported control {:#04x}/{:#04x} value={:#06x} index={:#06x} length={}",
m_setup[0], m_setup[1], LE16(m_setup, 2), LE16(m_setup, 4), LE16(m_setup, 6));
*actual = 8;
return m_control_stalled ? Result::Stalled : Result::Completed;
}
if (!m_control_valid || m_control_stalled)
return Result::Stalled;
const bool read = (m_setup[0] & 0x80) != 0;
if (buffer.empty())
{
if (direction != (read ? 1 : 2) || (!read && m_out.size() != LE16(m_setup, 6)))
return Result::Stalled;
const bool applied = ApplyControl(m_out);
m_control_valid = false;
return applied ? Result::Completed : Result::Stalled;
}
if (direction == 2 && read)
{
*actual = std::min(buffer.size(), m_control.size() - m_control_offset);
std::copy_n(m_control.begin() + m_control_offset, *actual, buffer.begin());
m_control_offset += *actual;
}
else if (direction == 1 && !read && m_out.size() + buffer.size() <= LE16(m_setup, 6))
{
m_out.insert(m_out.end(), buffer.begin(), buffer.end());
*actual = buffer.size();
}
else
return Result::Stalled;
return Result::Completed;
}
if (m_configuration == 0)
return Result::Stalled;
if (endpoint == 1 && direction == 2)
{
if (!m_link_event || m_software_mii)
return Result::Pending;
// Datasheet 4.1.4/6.3: the last four bytes are PHY register snapshots,
// selected by EEPROM word 0x0f, not unused padding.
const u16 first = ReadPHY((m_eeprom[0x0f] >> 8) & 0x1f);
const u16 second = ReadPHY(m_eeprom[0x0f] & 0x1f);
const std::array<u8, 8> event{0xa1,
0,
static_cast<u8>(LinkUp() ? 1 : 0),
0,
static_cast<u8>(first),
static_cast<u8>(first >> 8),
static_cast<u8>(second),
static_cast<u8>(second >> 8)};
*actual = std::min(buffer.size(), event.size());
std::copy_n(event.begin(), *actual, buffer.begin());
if (*actual == event.size())
{
m_link_event = false;
m_status_frames_remaining = INTERRUPT_INTERVAL_MS;
if (++m_interrupt_reports <= 8)
INFO_LOG_FMT(IOS_USB, "AX88772 status report: link={} PHY={:#06x}/{:#06x}", LinkUp(), first,
second);
}
return Result::Completed;
}
if (endpoint == 2 && direction == 2)
{
if (m_rx.empty())
return Result::Pending;
auto& packet = m_rx.front();
*actual = std::min(buffer.size(), packet.size() - m_rx_offset);
std::copy_n(packet.begin() + m_rx_offset, *actual, buffer.begin());
m_rx_offset += *actual;
if (m_rx_offset == packet.size())
{
m_rx.pop_front();
m_rx_offset = 0;
}
return Result::Completed;
}
if (endpoint == 3 && direction == 1)
{
const auto result = BulkOut(buffer);
if (result == Result::Completed)
*actual = buffer.size();
return result;
}
return Result::Stalled;
}
} // namespace IOS::LLE
+81
View File
@@ -0,0 +1,81 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <deque>
#include <functional>
#include <span>
#include <vector>
#include "Common/CommonTypes.h"
class PointerWrap;
namespace IOS::LLE
{
// USB full-speed AX88772A. IOS still executes its own USB, Ethernet and IP drivers.
// The backend exchanges Ethernet frames only; it never accesses guest memory or IOS IPC.
class AX88772
{
public:
enum class Result
{
Completed,
Pending,
Stalled
};
using Transmit = std::function<void(std::span<const u8>)>;
explicit AX88772(Transmit transmit);
void Reset();
void DoState(PointerWrap& p);
void SetLink(bool connected);
// One emulated full-speed USB frame (1 ms), not a host wall-clock tick.
void AdvanceUSBFrame();
void ReceiveFrame(std::span<const u8> frame);
Result Transfer(u8 endpoint, u8 direction, std::span<u8> buffer, size_t* actual);
u8 GetAddress() const { return m_address; }
private:
static constexpr u8 INTERRUPT_INTERVAL_MS = 10;
bool PrepareControl();
bool ApplyControl(std::span<const u8> data);
std::vector<u8> Descriptor(u16 value) const;
u16 ReadPHY(u16 reg) const;
bool LinkUp() const;
Result BulkOut(std::span<const u8> data);
Transmit m_transmit;
std::array<u8, 6> m_mac{0x02, 0x44, 0x4f, 0x4c, 0x00, 0x01};
std::array<u16, 256> m_eeprom{};
std::array<u16, 32> m_phy{};
std::array<u8, 8> m_multicast{};
std::array<u8, 3> m_ipg{0x15, 0x0c, 0x12};
std::array<u8, 8> m_setup{};
std::vector<u8> m_control;
std::vector<u8> m_out;
std::vector<u8> m_tx;
std::deque<std::vector<u8>> m_rx;
size_t m_control_offset = 0;
size_t m_rx_offset = 0;
u16 m_rx_control = 0;
u16 m_medium = 0;
u16 m_gpio = 0;
u8 m_monitor = 0;
u8 m_phy_select = 1;
u8 m_software_reset = 0x28;
u8 m_address = 0;
u8 m_configuration = 0;
bool m_control_valid = false;
bool m_control_stalled = false;
bool m_software_mii = false;
bool m_eeprom_writable = false;
bool m_connected = false;
bool m_link_event = true;
u8 m_status_frames_remaining = 0;
// Diagnostics only; not part of guest-visible/savestate state.
u32 m_control_log_count = 0;
u64 m_interrupt_reports = 0;
};
} // namespace IOS::LLE
@@ -0,0 +1,320 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/IOS/Starlet/SlirpNetwork.h"
#include <algorithm>
#include <cerrno>
#include <filesystem>
#include <limits>
#include <vector>
#define LIBSLIRP_STATIC
#include "libslirp.h"
#ifndef _WIN32
#include <poll.h>
#endif
#include "Common/DynamicLibrary.h"
#include "Common/Logging/Log.h"
#include "Common/Network.h"
#include "Common/StringUtil.h"
namespace IOS::LLE
{
namespace
{
#ifdef _WIN32
// WSAPoll rejects POLLPRI with WSAEINVAL, failing the entire poll array. Winsock
// uses POLLRDBAND for urgent data, and POLLIN combines normal and band events.
// Keep them distinct when translating to libslirp's IN/PRI flags.
constexpr short NATIVE_POLL_READ = POLLRDNORM;
constexpr short NATIVE_POLL_PRIORITY = POLLRDBAND;
#else
constexpr short NATIVE_POLL_READ = POLLIN;
constexpr short NATIVE_POLL_PRIORITY = POLLPRI;
#endif
// Header-only diagnostics: no HTTP contents, DNS names or other guest payloads.
void LogNetworkFrame(std::span<const u8> frame, const char* direction, u64 count)
{
if (count > 128 || frame.size() < 34 || frame[12] != 8 || frame[13] != 0 || (frame[14] >> 4) != 4)
return;
const auto ip = frame.subspan(14);
const auto word = [](std::span<const u8> bytes, size_t offset) {
return (u16(bytes[offset]) << 8) | bytes[offset + 1];
};
const auto dword = [&](std::span<const u8> bytes, size_t offset) {
return (u32(word(bytes, offset)) << 16) | word(bytes, offset + 2);
};
const size_t header_size = (ip[0] & 15) * 4;
const size_t total_size = word(ip, 2);
if (header_size < 20 || header_size > total_size || total_size > ip.size())
return;
const auto payload = ip.subspan(header_size, total_size - header_size);
std::string transport;
if ((word(ip, 6) & 0x1fff) != 0)
transport = "fragment";
else if (ip[9] == 6 && payload.size() >= 20)
transport = fmt::format("TCP {}->{} flags={:#04x} seq={} ack={} win={} header={}",
word(payload, 0), word(payload, 2), payload[13], dword(payload, 4),
dword(payload, 8), word(payload, 14), (payload[12] >> 4) * 4);
else if (ip[9] == 17 && payload.size() >= 8)
transport =
fmt::format("UDP {}->{} length={}", word(payload, 0), word(payload, 2), word(payload, 4));
else if (ip[9] == 1 && payload.size() >= 4)
transport = fmt::format("ICMP type={} code={}", payload[0], payload[1]);
INFO_LOG_FMT(IOS_NET,
"Starlet NAT {} #{} IPv4 {}.{}.{}.{} -> {}.{}.{}.{} proto={} bytes={} "
"IP-checksum={} {}",
direction, count, ip[12], ip[13], ip[14], ip[15], ip[16], ip[17], ip[18], ip[19],
ip[9], total_size,
Common::ComputeNetworkChecksum(ip.data(), static_cast<u16>(header_size)) == 0,
transport);
}
} // namespace
short SlirpSocketPolling::ToNativeEvents(int events)
{
short requested = 0;
if (events & SLIRP_POLL_IN)
requested |= NATIVE_POLL_READ;
if (events & SLIRP_POLL_OUT)
requested |= POLLOUT;
if (events & SLIRP_POLL_PRI)
requested |= NATIVE_POLL_PRIORITY;
return requested;
}
int SlirpSocketPolling::ToSlirpEvents(short events)
{
int ready = 0;
if (events & NATIVE_POLL_READ)
ready |= SLIRP_POLL_IN;
if (events & POLLOUT)
ready |= SLIRP_POLL_OUT;
if (events & NATIVE_POLL_PRIORITY)
ready |= SLIRP_POLL_PRI;
if (events & (POLLERR | POLLNVAL))
ready |= SLIRP_POLL_ERR;
if (events & POLLHUP)
ready |= SLIRP_POLL_HUP;
return ready;
}
struct SlirpNetwork::Impl
{
Common::DynamicLibrary library;
decltype(&slirp_new) create = nullptr;
decltype(&slirp_cleanup) cleanup = nullptr;
decltype(&slirp_input) input = nullptr;
decltype(&slirp_pollfds_fill_socket) fill = nullptr;
decltype(&slirp_pollfds_poll) poll = nullptr;
decltype(&slirp_version_string) version = nullptr;
Slirp* slirp = nullptr;
SlirpCb callbacks{};
Receive receive;
s64 now_ns = 0;
u64 last_arm_cycles = 0;
u64 elapsed_arm_cycles = 0;
u64 tx_frames = 0;
u64 rx_frames = 0;
u64 poll_errors = 0;
#ifdef _WIN32
bool winsock = false;
std::vector<WSAPOLLFD> sockets;
#else
std::vector<pollfd> sockets;
#endif
struct Timer
{
SlirpTimerCb callback;
void* opaque;
s64 deadline = std::numeric_limits<s64>::max();
};
std::vector<std::unique_ptr<Timer>> timers;
~Impl()
{
if (slirp)
cleanup(slirp);
#ifdef _WIN32
if (winsock)
WSACleanup();
#endif
}
};
SlirpNetwork::SlirpNetwork() = default;
SlirpNetwork::~SlirpNetwork() = default;
bool SlirpNetwork::Start(const std::string& directory, Receive receive, std::string* error)
{
m_impl.reset();
auto impl = std::make_unique<Impl>();
const auto fail = [&](const std::string& message) {
if (error)
*error = message;
return false;
};
#ifdef _WIN32
WSADATA wsadata{};
if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
return fail("Could not initialize Winsock for the Ethernet NAT backend");
impl->winsock = true;
// Search dependencies alongside this exact DLL, not via a modified process/global PATH.
const auto path = std::filesystem::path(UTF8ToWString(directory + "/libslirp-0.dll"))
.lexically_normal()
.wstring();
impl->library = LoadLibraryExW(
path.c_str(), nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
if (!impl->library.IsOpen())
return fail(fmt::format("Could not load libslirp NAT runtime (Windows error {}): {}. "
"Run Setup-Wii-LLE-Network.ps1 for this build.",
GetLastError(), WStringToUTF8(path)));
#elif defined(__APPLE__)
impl->library.Open((directory + "/libslirp.0.dylib").c_str());
#else
impl->library.Open((directory + "/libslirp.so.0").c_str());
#endif
if (!impl->library.IsOpen())
return fail("libslirp NAT runtime is missing; run Setup-Wii-LLE-Network.ps1 for this build");
if (!impl->library.GetSymbol("slirp_new", &impl->create) ||
!impl->library.GetSymbol("slirp_cleanup", &impl->cleanup) ||
!impl->library.GetSymbol("slirp_input", &impl->input) ||
!impl->library.GetSymbol("slirp_pollfds_fill_socket", &impl->fill) ||
!impl->library.GetSymbol("slirp_pollfds_poll", &impl->poll) ||
!impl->library.GetSymbol("slirp_version_string", &impl->version))
return fail("The Ethernet NAT backend requires libslirp 4.9 or newer");
impl->receive = std::move(receive);
auto& cb = impl->callbacks;
cb.send_packet = [](const void* data, size_t length, void* opaque) -> slirp_ssize_t {
auto& self = *static_cast<Impl*>(opaque);
if (length >= 14 && length <= 1518 && self.receive)
{
if (++self.rx_frames <= 8)
INFO_LOG_FMT(IOS_NET, "Starlet NAT -> USB Ethernet: frame {} ({} bytes)", self.rx_frames,
length);
LogNetworkFrame({static_cast<const u8*>(data), length}, "RX", self.rx_frames);
self.receive({static_cast<const u8*>(data), length});
}
return static_cast<slirp_ssize_t>(length);
};
cb.guest_error = [](const char* message, void*) {
WARN_LOG_FMT(IOS_NET, "Starlet NAT: {}", message);
};
cb.clock_get_ns = [](void* opaque) -> int64_t { return static_cast<Impl*>(opaque)->now_ns; };
cb.timer_new = [](SlirpTimerCb callback, void* callback_opaque, void* opaque) -> void* {
auto& timers = static_cast<Impl*>(opaque)->timers;
timers.push_back(std::make_unique<Impl::Timer>(Impl::Timer{callback, callback_opaque}));
return timers.back().get();
};
cb.timer_free = [](void* timer, void* opaque) {
auto& timers = static_cast<Impl*>(opaque)->timers;
std::erase_if(timers, [timer](const auto& entry) { return entry.get() == timer; });
};
cb.timer_mod = [](void* timer, int64_t expiry, void*) {
static_cast<Impl::Timer*>(timer)->deadline = expiry;
};
cb.register_poll_socket = [](slirp_os_socket, void*) {};
cb.unregister_poll_socket = [](slirp_os_socket, void*) {};
cb.notify = [](void*) {}; // Nonblocking polls at emulated USB frame boundaries.
SlirpConfig config{};
config.version = 6;
config.in_enabled = true;
config.vnetwork.s_addr = htonl(0x0a000200);
config.vnetmask.s_addr = htonl(0xffffff00);
config.vhost.s_addr = htonl(0x0a000202);
config.vdhcp_start.s_addr = htonl(0x0a00020f);
config.vnameserver.s_addr = htonl(0x0a000203);
config.vhostname = "dolphin-wii";
config.if_mtu = config.if_mru = 1500;
config.disable_host_loopback = true;
// No host services, TFTP file sharing, command forwarding or inbound port mappings.
config.enable_emu = false;
impl->slirp = impl->create(&config, &cb, impl.get());
if (!impl->slirp)
return fail("libslirp could not create the Ethernet NAT network");
INFO_LOG_FMT(IOS_NET, "Starlet Ethernet NAT initialized: libslirp {}, subnet 10.0.2.0/24",
impl->version());
m_impl = std::move(impl);
return true;
}
void SlirpNetwork::Input(std::span<const u8> frame)
{
if (m_impl && frame.size() >= 14 && frame.size() <= 1518)
{
if (++m_impl->tx_frames <= 8)
INFO_LOG_FMT(IOS_NET, "Starlet USB Ethernet -> NAT: frame {} ({} bytes, type {:#06x})",
m_impl->tx_frames, frame.size(), (u16(frame[12]) << 8) | frame[13]);
LogNetworkFrame(frame, "TX", m_impl->tx_frames);
m_impl->input(m_impl->slirp, frame.data(), static_cast<int>(frame.size()));
}
}
void SlirpNetwork::Poll(u64 arm_cycles)
{
if (!m_impl)
return;
auto& self = *m_impl;
// A console reset restarts the ARM cycle counter, not the NAT's monotonic clock.
self.elapsed_arm_cycles +=
arm_cycles >= self.last_arm_cycles ? arm_cycles - self.last_arm_cycles : arm_cycles;
self.last_arm_cycles = arm_cycles;
self.now_ns = static_cast<s64>((self.elapsed_arm_cycles / 243000000) * 1000000000 +
(self.elapsed_arm_cycles % 243000000) * 1000000000 / 243000000);
self.sockets.clear();
uint32_t timeout = 0;
self.fill(
self.slirp, &timeout,
[](slirp_os_socket socket, int events, void* opaque) -> int {
auto& sockets = static_cast<Impl*>(opaque)->sockets;
sockets.push_back({socket, SlirpSocketPolling::ToNativeEvents(events), 0});
return static_cast<int>(sockets.size() - 1);
},
&self);
int result = 0;
if (!self.sockets.empty())
{
#ifdef _WIN32
result = WSAPoll(self.sockets.data(), static_cast<ULONG>(self.sockets.size()), 0);
#else
result = ::poll(self.sockets.data(), self.sockets.size(), 0);
#endif
if (result < 0)
{
#ifdef _WIN32
const int error = WSAGetLastError();
#else
const int error = errno;
#endif
if (++self.poll_errors <= 8)
WARN_LOG_FMT(IOS_NET, "Starlet NAT socket poll failed: error {} ({} sockets)", error,
self.sockets.size());
}
}
self.poll(
self.slirp, result < 0,
[](int index, void* opaque) -> int {
const auto& sockets = static_cast<Impl*>(opaque)->sockets;
if (index < 0 || static_cast<size_t>(index) >= sockets.size())
return 0;
return SlirpSocketPolling::ToSlirpEvents(sockets[index].revents);
},
&self);
for (size_t count = 0; count < 64; ++count)
{
const auto timer = std::find_if(self.timers.begin(), self.timers.end(), [&](const auto& entry) {
return entry->deadline <= self.now_ns / 1000000;
});
if (timer == self.timers.end())
break;
const auto callback = (*timer)->callback;
void* opaque = (*timer)->opaque;
(*timer)->deadline = std::numeric_limits<s64>::max();
callback(opaque);
}
}
} // namespace IOS::LLE
@@ -0,0 +1,38 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <functional>
#include <memory>
#include <span>
#include <string>
#include "Common/CommonTypes.h"
namespace IOS::LLE
{
// Platform poll flags differ between Winsock and POSIX. Kept separately from
// the NAT lifetime so the real host polling behavior can be regression-tested.
namespace SlirpSocketPolling
{
short ToNativeEvents(int events);
int ToSlirpEvents(short events);
} // namespace SlirpSocketPolling
// Nonblocking Ethernet NAT backend. All calls and callbacks stay on the emulation thread.
class SlirpNetwork
{
public:
using Receive = std::function<void(std::span<const u8>)>;
SlirpNetwork();
~SlirpNetwork();
bool Start(const std::string& directory, Receive receive, std::string* error);
void Input(std::span<const u8> frame);
void Poll(u64 arm_cycles);
private:
struct Impl;
std::unique_ptr<Impl> m_impl;
};
} // namespace IOS::LLE
+189 -30
View File
@@ -462,6 +462,8 @@ StarletMemory::StarletMemory(Core::System& system) : m_system(system)
bool StarletMemory::Init(const std::string& dump_directory, std::string* error)
{
m_ethernet_network.reset();
m_ethernet.reset();
m_nand_journal.reset();
m_nand_overlay.clear();
const std::string boot_path = PathInDirectory(dump_directory, "boot0.bin");
@@ -554,6 +556,11 @@ bool StarletMemory::Init(const std::string& dump_directory, std::string* error)
// replacing them immediately after reading BT.DINF leaves a short-lived
// callback pointing at a destroyed device.
m_initialized = false;
if (Config::Get(Config::MAIN_WII_LLE_ETHERNET))
{
if (!EnableEthernetNAT(File::GetExeDirectory() + "/Network", error))
return false;
}
InitSDCard();
Reset();
if (!InstallEmulatedWiimotePairings())
@@ -569,6 +576,30 @@ bool StarletMemory::Init(const std::string& dump_directory, std::string* error)
return true;
}
bool StarletMemory::EnableEthernetNAT(const std::string& runtime_directory, std::string* error)
{
m_ethernet_network.reset();
m_ethernet = std::make_unique<AX88772>([this](std::span<const u8> frame) {
if (m_ethernet_network)
m_ethernet_network->Input(frame);
});
m_ethernet_network = std::make_unique<SlirpNetwork>();
if (!m_ethernet_network->Start(
runtime_directory, [this](std::span<const u8> frame) { m_ethernet->ReceiveFrame(frame); },
error))
{
m_ethernet_network.reset();
m_ethernet.reset();
return false;
}
m_ethernet->SetLink(true);
WriteRegister(EHCI_BASE + EHCI_PORT_STATUS_1,
EHCI_PORT_CONNECT_STATUS | EHCI_PORT_CONNECT_CHANGE | EHCI_PORT_POWER | (2U << 10));
UpdateEthernetPortRouting();
INFO_LOG_FMT(IOS_USB, "Starlet external USB port 1: virtual AX88772A, full-speed, Ethernet NAT");
return true;
}
void StarletMemory::InitSDCard()
{
m_sd_card.Close();
@@ -599,6 +630,8 @@ void StarletMemory::InitSDCard()
void StarletMemory::Reset()
{
if (m_ethernet)
m_ethernet->Reset();
const std::lock_guard wifi_sdio_lock(m_wifi_sdio_register_mutex);
for (auto& wiimote : m_wiimotes)
{
@@ -785,6 +818,31 @@ void StarletMemory::DoState(PointerWrap& p)
p.Do(m_initialized);
p.Do(m_boot0_mapped);
p.Do(m_sram_split_mode);
bool ethernet_enabled = m_ethernet != nullptr;
p.Do(ethernet_enabled);
if (p.IsReadMode() && ethernet_enabled != (m_ethernet != nullptr))
{
PanicAlertFmtT("This savestate requires the same Wii LLE Ethernet setting. Aborting load.");
p.SetVerifyMode();
return;
}
if (m_ethernet)
{
m_ethernet->DoState(p);
if (p.IsReadMode())
{
std::string error;
if (!m_ethernet_network->Start(
File::GetExeDirectory() + "/Network",
[this](std::span<const u8> frame) { m_ethernet->ReceiveFrame(frame); }, &error))
{
PanicAlertFmtT("Could not restart Ethernet NAT: {0}", error);
p.SetVerifyMode();
return;
}
WARN_LOG_FMT(IOS_NET, "Restored AX88772 state; host TCP/UDP connections were reset");
}
}
if (p.IsReadMode() && m_nand_journal && !m_nand_journal->Append(m_nand_overlay, true))
{
PanicAlertFmtT("Could not persist the NAND restored from the savestate. "
@@ -1008,6 +1066,32 @@ void StarletMemory::WriteMapped8(u32 address, u8 value)
std::memory_order_relaxed);
}
u8 StarletMemory::ReadDMA8(u32 address) const
{
// HW_SRNPROT.SM swaps the CPU's SRAM apertures, not the AHB masters' physical
// banks. Firmware (e.g. MINI's dma_addr) already removes that swap before
// programming a DMA register. Applying GetSRAMOffset again can select the
// wrong bank or discard IOS's AES output into the CPU's split-mode hole.
if (address >= SRAM_BASE && address < SRAM_BASE + SRAM_WINDOW_SIZE)
{
const u32 offset = address - SRAM_BASE;
return offset < SRAM_SIZE ? m_sram[offset] : 0;
}
return ReadMapped8(address);
}
void StarletMemory::WriteDMA8(u32 address, u8 value)
{
if (address >= SRAM_BASE && address < SRAM_BASE + SRAM_WINDOW_SIZE)
{
const u32 offset = address - SRAM_BASE;
if (offset < SRAM_SIZE)
m_sram[offset] = value;
return;
}
WriteMapped8(address, value);
}
void StarletMemory::ClearRegisterCache()
{
for (auto& entry : m_register_cache)
@@ -1064,9 +1148,35 @@ void StarletMemory::ResetEHCIController(bool preserve_phy_registers)
m_ehci_running = false;
WriteRegister(EHCI_BASE + EHCI_USB_COMMAND, EHCI_COMMAND_HOLLYWOOD_RESET_VALUE);
WriteRegister(EHCI_BASE + EHCI_USB_STATUS, EHCI_STATUS_HALTED);
if (m_ethernet)
WriteRegister(EHCI_BASE + EHCI_PORT_STATUS_1, EHCI_PORT_CONNECT_STATUS |
EHCI_PORT_CONNECT_CHANGE | EHCI_PORT_POWER |
(2U << 10));
UpdateEthernetPortRouting();
UpdateEHCIInterrupt();
}
void StarletMemory::UpdateEthernetPortRouting()
{
if (!m_ethernet)
return;
const u32 ehci_port = ReadRegister(EHCI_BASE + EHCI_PORT_STATUS_1);
const bool companion = (ReadRegister(EHCI_BASE + EHCI_CONFIGURED_FLAG) & 1) == 0 ||
(ehci_port & EHCI_PORT_OWNER) != 0;
const u32 address = OHCI_BASES[0] + OHCI_RH_PORT_STATUS_1;
const u32 old = ReadRegister(address);
u32 port = old;
if (companion && (ehci_port & EHCI_PORT_POWER) != 0)
port |= OHCI_PORT_CURRENT_CONNECT_STATUS | OHCI_PORT_POWER_STATUS;
else
port &= ~(OHCI_PORT_CURRENT_CONNECT_STATUS | OHCI_PORT_ENABLE_STATUS);
if ((old ^ port) & OHCI_PORT_CURRENT_CONNECT_STATUS)
port |= OHCI_PORT_CONNECT_STATUS_CHANGE;
WriteRegister(address, port);
if (port & OHCI_PORT_CHANGE_MASK)
SetOHCIInterruptStatus(0, OHCI_INTERRUPT_ROOT_HUB_STATUS_CHANGE);
}
void StarletMemory::SetEHCIInterruptStatus(u32 status)
{
WriteRegister(EHCI_BASE + EHCI_USB_STATUS,
@@ -1142,6 +1252,10 @@ void StarletMemory::HandleEHCIWrite(u32 address)
case EHCI_ASYNC_LIST_ADDRESS:
WriteRegister(address, value & 0xffffffe0);
break;
case EHCI_CONFIGURED_FLAG:
WriteRegister(address, value & 1);
UpdateEthernetPortRouting();
break;
case EHCI_PORT_STATUS_1:
case EHCI_PORT_STATUS_2:
{
@@ -1152,7 +1266,18 @@ void StarletMemory::HandleEHCIWrite(u32 address)
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);
if (m_ethernet && offset == EHCI_PORT_STATUS_1)
{
// The full-speed device belongs to OHCI0 after the EHCI owner handoff.
port |= EHCI_PORT_CONNECT_STATUS | (2U << 10);
if (value & EHCI_PORT_RESET)
{
port = (port | EHCI_PORT_OWNER) & ~EHCI_PORT_RESET;
m_ethernet->Reset();
}
}
WriteRegister(address, port);
UpdateEthernetPortRouting();
UpdateEHCIInterrupt();
break;
}
@@ -1262,6 +1387,8 @@ void StarletMemory::ResetOHCIController(size_t controller)
// 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 == 0)
UpdateEthernetPortRouting();
if (controller == 1)
{
WriteRegister(base + OHCI_RH_PORT_STATUS_1, OHCI_PORT_CURRENT_CONNECT_STATUS |
@@ -1393,6 +1520,8 @@ void StarletMemory::HandleOHCIWrite(size_t controller, u32 address)
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 == 0 && port_index == 0 && m_ethernet)
m_ethernet->Reset();
if (controller == 1 && port_index == 0)
{
m_ohci1_device_address = 0;
@@ -1408,6 +1537,10 @@ void StarletMemory::HandleOHCIWrite(size_t controller, u32 address)
if ((value & (1U << 8)) != 0)
{
port |= OHCI_PORT_POWER_STATUS;
if (controller == 0 && port_index == 0 && m_ethernet &&
((ReadRegister(EHCI_BASE + EHCI_CONFIGURED_FLAG) & 1) == 0 ||
(ReadRegister(EHCI_BASE + EHCI_PORT_STATUS_1) & EHCI_PORT_OWNER) != 0))
port |= OHCI_PORT_CURRENT_CONNECT_STATUS | OHCI_PORT_CONNECT_STATUS_CHANGE;
if (controller == 1 && port_index == 0 && (port & OHCI_PORT_CURRENT_CONNECT_STATUS) == 0)
m_ohci1_attach_delay_frames = OHCI1_ATTACH_DELAY_FRAMES;
}
@@ -1446,6 +1579,10 @@ void StarletMemory::AdvanceOHCI(size_t controller, u64 cycles)
const u16 old_frame = static_cast<u16>(ReadRegister(base + OHCI_FRAME_NUMBER));
const u16 frame = static_cast<u16>(old_frame + 1);
WriteRegister(base + OHCI_FRAME_NUMBER, frame);
if (controller == 0 && m_ethernet)
m_ethernet->AdvanceUSBFrame();
if (controller == 0 && m_ethernet_network)
m_ethernet_network->Poll(m_arm_cycles);
for (size_t port_index = 0; port_index < m_ohci_port_reset_frames[controller].size();
++port_index)
@@ -1484,8 +1621,8 @@ void StarletMemory::AdvanceOHCI(size_t controller, u64 cycles)
const u32 hcca = ReadRegister(base + OHCI_HCCA);
if (hcca != 0)
{
WriteMapped8(hcca + 0x80, static_cast<u8>(frame));
WriteMapped8(hcca + 0x81, static_cast<u8>(frame >> 8));
WriteDMA8(hcca + 0x80, static_cast<u8>(frame));
WriteDMA8(hcca + 0x81, static_cast<u8>(frame >> 8));
}
ProcessOHCISchedules(controller);
SetOHCIInterruptStatus(controller, OHCI_INTERRUPT_START_OF_FRAME);
@@ -1496,17 +1633,17 @@ void StarletMemory::AdvanceOHCI(size_t controller, u64 cycles)
u32 StarletMemory::ReadOHCIMemory32(u32 address) const
{
return static_cast<u32>(ReadMapped8(address)) | static_cast<u32>(ReadMapped8(address + 1)) << 8 |
static_cast<u32>(ReadMapped8(address + 2)) << 16 |
static_cast<u32>(ReadMapped8(address + 3)) << 24;
return static_cast<u32>(ReadDMA8(address)) | static_cast<u32>(ReadDMA8(address + 1)) << 8 |
static_cast<u32>(ReadDMA8(address + 2)) << 16 |
static_cast<u32>(ReadDMA8(address + 3)) << 24;
}
void StarletMemory::WriteOHCIMemory32(u32 address, u32 value)
{
WriteMapped8(address, static_cast<u8>(value));
WriteMapped8(address + 1, static_cast<u8>(value >> 8));
WriteMapped8(address + 2, static_cast<u8>(value >> 16));
WriteMapped8(address + 3, static_cast<u8>(value >> 24));
WriteDMA8(address, static_cast<u8>(value));
WriteDMA8(address + 1, static_cast<u8>(value >> 8));
WriteDMA8(address + 2, static_cast<u8>(value >> 16));
WriteDMA8(address + 3, static_cast<u8>(value >> 24));
}
std::vector<u8> StarletMemory::ReadOHCIBuffer(u32 current_buffer, u32 buffer_end) const
@@ -1525,10 +1662,10 @@ std::vector<u8> StarletMemory::ReadOHCIBuffer(u32 current_buffer, u32 buffer_end
std::vector<u8> buffer(first_size + second_size);
for (size_t i = 0; i < first_size; ++i)
buffer[i] = ReadMapped8(current_buffer + static_cast<u32>(i));
buffer[i] = ReadDMA8(current_buffer + static_cast<u32>(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<u32>(i));
buffer[first_size + i] = ReadDMA8(second_page + static_cast<u32>(i));
return buffer;
}
@@ -1542,14 +1679,14 @@ void StarletMemory::WriteOHCIBuffer(u32 current_buffer, u32 buffer_end, const u8
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<u32>(i), data[i]);
WriteDMA8(current_buffer + static_cast<u32>(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<size_t>((buffer_end & 0xfff) + 1));
for (size_t i = 0; i < second_size; ++i)
WriteMapped8(second_page + static_cast<u32>(i), data[first_size + i]);
WriteDMA8(second_page + static_cast<u32>(i), data[first_size + i]);
}
}
@@ -2059,6 +2196,20 @@ StarletMemory::OHCITransferResult StarletMemory::ExecuteOHCITransfer(size_t cont
size_t* actual_length)
{
*actual_length = 0;
if (controller == 0 && m_ethernet)
{
const auto result = m_ethernet->Transfer(static_cast<u8>(endpoint), static_cast<u8>(direction),
*buffer, actual_length);
switch (result)
{
case AX88772::Result::Completed:
return OHCITransferResult::Completed;
case AX88772::Result::Pending:
return OHCITransferResult::Pending;
case AX88772::Result::Stalled:
return OHCITransferResult::Stalled;
}
}
if (controller != 1)
return OHCITransferResult::Stalled;
@@ -2244,7 +2395,14 @@ bool StarletMemory::ProcessOHCIEndpoint(size_t controller, u32 endpoint_address,
size_t actual_length = 0;
OHCITransferResult result = OHCITransferResult::Stalled;
if (controller == 1 && function_address == m_ohci1_device_address && direction != 3)
const bool ethernet_ready =
controller == 0 && m_ethernet &&
(ReadRegister(base + OHCI_RH_PORT_STATUS_1) &
(OHCI_PORT_CURRENT_CONNECT_STATUS | OHCI_PORT_ENABLE_STATUS | OHCI_PORT_SUSPEND_STATUS)) ==
(OHCI_PORT_CURRENT_CONNECT_STATUS | OHCI_PORT_ENABLE_STATUS);
const u32 device_address =
controller == 0 && m_ethernet ? m_ethernet->GetAddress() : m_ohci1_device_address;
if ((controller == 1 || ethernet_ready) && function_address == device_address && direction != 3)
result = ExecuteOHCITransfer(controller, endpoint, direction, &buffer, &actual_length);
else if (controller == 1 && direction != 3)
result = OHCITransferResult::Stalled;
@@ -2254,8 +2412,9 @@ bool StarletMemory::ProcessOHCIEndpoint(size_t controller, u32 endpoint_address,
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;
condition_code = (controller == 1 || ethernet_ready) && function_address == 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
@@ -2749,9 +2908,9 @@ void StarletMemory::ExecuteWiFiSDIOCommand(u32 command_and_mode)
for (u32 i = 0; i < size; ++i)
{
if (write)
WriteWiFiSDIOByte(function, register_address, ReadMapped8(dma_address + i));
WriteWiFiSDIOByte(function, register_address, ReadDMA8(dma_address + i));
else
WriteMapped8(dma_address + i, ReadWiFiSDIOByte(function, register_address));
WriteDMA8(dma_address + i, ReadWiFiSDIOByte(function, register_address));
if (increment)
register_address = (register_address + 1) & 0x1ffff;
}
@@ -2923,14 +3082,14 @@ bool StarletMemory::TransferSDCardBlocks(bool read, u32 argument, u32 block_size
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<u32>(i), data[i]);
WriteDMA8(dma_address + static_cast<u32>(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<u32>(i));
data[i] = ReadDMA8(dma_address + static_cast<u32>(i));
if (!m_sd_card.WriteBytes(data.data(), data.size()))
return false;
}
@@ -2985,7 +3144,7 @@ void StarletMemory::ExecuteSDHCCommand(u32 command_and_mode)
}
constexpr std::array<u8, 8> scr = {0x02, 0x05, 0, 0, 0, 0, 0, 0};
for (size_t i = 0; i < scr.size(); ++i)
WriteMapped8(dma_address + static_cast<u32>(i), scr[i]);
WriteDMA8(dma_address + static_cast<u32>(i), scr[i]);
WriteRegister(SDHC_DMA_ADDRESS, dma_address + static_cast<u32>(scr.size()));
transfer_complete = true;
break;
@@ -4153,7 +4312,7 @@ bool StarletMemory::ReadNANDPage(u32 command)
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]);
WriteDMA8(destination, raw[raw_offset]);
}
if (command & NAND_CTRL_ECC)
@@ -4161,7 +4320,7 @@ bool StarletMemory::ReadNANDPage(u32 command)
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]);
WriteDMA8(calculated_ecc_address + i, ecc[i]);
}
return true;
}
@@ -4177,7 +4336,7 @@ bool StarletMemory::ReadNANDID(u32 command)
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()]);
WriteDMA8(data_address + i, NAND_CHIP_ID[i % NAND_CHIP_ID.size()]);
return true;
}
@@ -4192,7 +4351,7 @@ bool StarletMemory::ReadNANDStatus(u32 command)
const u32 data_address = ReadRegister(NAND_DATA) & ~0xfU;
for (u32 i = 0; i < length; ++i)
WriteMapped8(data_address + i, m_nand_status);
WriteDMA8(data_address + i, m_nand_status);
return true;
}
@@ -4230,7 +4389,7 @@ bool StarletMemory::StageNANDProgram(u32 command, bool random_data_input)
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);
m_nand_program_data[raw_offset] = ReadDMA8(source);
}
if ((command & NAND_CTRL_ECC) != 0 && column == 0 && length >= NAND_PAGE_DATA_SIZE)
@@ -4238,7 +4397,7 @@ bool StarletMemory::StageNANDProgram(u32 command, bool random_data_input)
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]);
WriteDMA8(calculated_ecc_address + i, ecc[i]);
m_nand_program_ecc_enabled = true;
}
return true;
@@ -4409,7 +4568,7 @@ void StarletMemory::ExecuteAESCommand(u32 command)
std::vector<u8> input(size);
std::vector<u8> output(size);
for (size_t i = 0; i < size; ++i)
input[i] = ReadMapped8(source + static_cast<u32>(i));
input[i] = ReadDMA8(source + static_cast<u32>(i));
std::array<u8, 16> next_iv{};
bool succeeded = true;
@@ -4430,7 +4589,7 @@ void StarletMemory::ExecuteAESCommand(u32 command)
if ((command & AES_CTRL_ENABLE) != 0)
m_aes_iv = next_iv;
for (size_t i = 0; i < size; ++i)
WriteMapped8(destination + static_cast<u32>(i), output[i]);
WriteDMA8(destination + static_cast<u32>(i), output[i]);
WriteRegister(AES_SRC, source + static_cast<u32>(size));
WriteRegister(AES_DEST, destination + static_cast<u32>(size));
}
@@ -4517,7 +4676,7 @@ void StarletMemory::ExecuteSHACommand(u32 command)
for (u32 i = 0; i < blocks; ++i)
{
for (u32 j = 0; j < block.size(); ++j)
block[j] = ReadMapped8(source + i * 64 + j);
block[j] = ReadDMA8(source + i * 64 + j);
CompressSHA1(block.data());
}
for (u32 i = 0; i < m_sha_state.size(); ++i)
@@ -17,7 +17,9 @@
#include "Common/CommonTypes.h"
#include "Common/IOFile.h"
#include "Core/IOS/Starlet/ARMCore.h"
#include "Core/IOS/Starlet/AX88772.h"
#include "Core/IOS/Starlet/NANDJournal.h"
#include "Core/IOS/Starlet/SlirpNetwork.h"
#include "Core/IOS/USB/Bluetooth/WiimoteDevice.h"
class PointerWrap;
@@ -74,6 +76,7 @@ public:
u64 GetCycles() const { return m_arm_cycles; }
std::optional<u32> TryReadBroadwayResetInstruction(u32 address) const;
void SetWiimoteSource(size_t index, WiimoteCommon::HIDWiimote* source);
bool EnableEthernetNAT(const std::string& runtime_directory, std::string* error);
private:
static constexpr u32 NAND_PAGE_DATA_SIZE = 0x800;
@@ -91,6 +94,8 @@ private:
u8 ReadMapped8(u32 address) const;
void WriteMapped8(u32 address, u8 value);
u8 ReadDMA8(u32 address) const;
void WriteDMA8(u32 address, u8 value);
bool IsBootROMAddress(u32 address) const;
u32 GetBootROMOffset(u32 address) const;
u32 GetSRAMOffset(u32 address) const;
@@ -127,6 +132,7 @@ private:
void SetEHCIInterruptStatus(u32 status);
void UpdateEHCIInterrupt();
void AdvanceEHCI(u64 cycles);
void UpdateEthernetPortRouting();
static std::optional<size_t> GetOHCIControllerIndex(u32 address);
u32 ReadOHCIRegister(size_t controller, u32 address) const;
void HandleOHCIWrite(size_t controller, u32 address);
@@ -309,5 +315,7 @@ private:
bool m_sram_split_mode = false;
// Host persistence is not serialized; savestates contain the complete overlay above.
std::unique_ptr<NANDJournal> m_nand_journal;
std::unique_ptr<AX88772> m_ethernet;
std::unique_ptr<SlirpNetwork> m_ethernet_network;
};
} // namespace IOS::LLE
+1 -1
View File
@@ -97,7 +97,7 @@ struct CompressAndDumpStateArgs
static Common::WorkQueueThreadSP<CompressAndDumpStateArgs> s_compress_and_dump_thread;
// Don't forget to increase this after doing changes on the savestate system
constexpr u32 STATE_VERSION = 193; // Starlet HW_TIMER counter offset.
constexpr u32 STATE_VERSION = 196; // Correct Hollywood DI/reset pending interrupt bit mapping.
// Increase this if the StateExtendedHeader definition changes
constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217
+2
View File
@@ -16,6 +16,8 @@ add_dolphin_test(ESFormatsTest IOS/ES/FormatsTest.cpp)
add_dolphin_test(StarletARMCoreTest IOS/Starlet/ARMCoreTest.cpp)
add_dolphin_test(StarletNANDJournalTest IOS/Starlet/NANDJournalTest.cpp)
add_dolphin_test(StarletEthernetTest IOS/Starlet/AX88772Test.cpp)
target_include_directories(StarletEthernetTest PRIVATE ${PROJECT_SOURCE_DIR}/Externals/libslirp/include)
add_dolphin_test(FileSystemTest IOS/FS/FileSystemTest.cpp)
@@ -47,6 +47,8 @@ public:
u16 Read16(u32 address) override
{
++m_read16_count;
if (address >= MMIO_WORD_ADDRESS && address <= MMIO_WORD_ADDRESS + 2)
return static_cast<u16>(m_mmio_word >> (16 - (address & 3) * 8));
const size_t offset = ToOffset(address);
EXPECT_LT(offset + 1, m_memory.size());
if (offset + 1 >= m_memory.size())
@@ -333,6 +335,137 @@ TEST(StarletSRAM, WideAccessesPreserveAliasesSplitMappingAndBoot0Protection)
EXPECT_EQ(memory.Read32(sram_low + 0x20), 0xaabbccddu);
}
TEST(StarletDMA, AESUsesPhysicalSRAMBanksRegardlessOfCPUSplit)
{
// NIST SP 800-38A F.2.1, AES-128 CBC block 1. Exercise the actual MMIO engine,
// including IOS's in-place SRAM-stack operation (CPU fffff080 -> DMA 0d40f080).
constexpr std::array<u32, 4> key = {0x2b7e1516, 0x28aed2a6, 0xabf71588, 0x09cf4f3c};
constexpr std::array<u32, 4> iv = {0x00010203, 0x04050607, 0x08090a0b, 0x0c0d0e0f};
constexpr std::array<u32, 4> plaintext = {0x6bc1bee2, 0x2e409f96, 0xe93d7e11, 0x7393172a};
constexpr std::array<u32, 4> ciphertext = {0x7649abac, 0x8119b246, 0xcee98e9b, 0x12e9197d};
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
for (const bool split : {false, true})
{
for (const u32 offset : {0xf080u, 0x10020u, 0x17ff0u})
{
for (const bool decrypt : {false, true})
{
SCOPED_TRACE(::testing::Message()
<< "split=" << split << " offset=" << offset << " decrypt=" << decrypt);
system.GetWiiIPC().Reset();
StarletMemory memory(system);
memory.Reset();
memory.Write32(0x0d800060, split ? 0x20 : 0);
// Use the non-ROM high aperture for both SRAM A and B.
const u32 cpu_address = StarletMemory::SRAM_MIRROR_BASE + (offset ^ (split ? 0x10000 : 0));
const u32 dma_address = StarletMemory::SRAM_BASE + offset;
for (u32 i = 0; i < 4; ++i)
{
memory.Write32(cpu_address + i * 4, (decrypt ? ciphertext : plaintext)[i]);
memory.Write32(0x0d02000c, key[i]);
memory.Write32(0x0d020010, iv[i]);
}
memory.Write32(0x0d020004, dma_address);
memory.Write32(0x0d020008, dma_address);
const u32 command = 0xd0000000 | (decrypt ? 0x08000000 : 0);
memory.Write32(0x0d020000, command);
for (u32 i = 0; i < 4; ++i)
EXPECT_EQ(memory.Read32(cpu_address + i * 4), (decrypt ? plaintext : ciphertext)[i]);
EXPECT_EQ(memory.Read32(0x0d020004), dma_address + 16);
EXPECT_EQ(memory.Read32(0x0d020008), dma_address + 16);
EXPECT_EQ(memory.Read32(0x0d020000), command & ~0x80000000u);
EXPECT_NE(memory.Read32(0x0d800038) & (1U << 2), 0u);
}
}
}
}
TEST(StarletDMA, CopyCrossesPhysicalBankBoundaryButDoesNotWrapPastSRAM)
{
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
for (const bool split : {false, true})
{
system.GetWiiIPC().Reset();
StarletMemory memory(system);
memory.Reset();
memory.Write32(0x0d800060, split ? 0x20 : 0);
const auto cpu_address = [split](u32 offset) {
return StarletMemory::SRAM_MIRROR_BASE + (offset ^ (split ? 0x10000 : 0));
};
for (u32 i = 0; i < StarletMemory::SRAM_SIZE; ++i)
memory.Write8(cpu_address(i), 0x5a);
for (u32 i = 0; i < 32; ++i)
memory.Write8(cpu_address(0xfff0 + i), static_cast<u8>(i + 1));
// Copy crosses A -> B on input, then B -> unmapped space on output.
memory.Write32(0x0d020004, 0x0d40fff0);
memory.Write32(0x0d020008, 0x0d417ff0);
memory.Write32(0x0d020000, 0x80000001); // Two blocks, AES disabled.
for (u32 i = 0; i < StarletMemory::SRAM_SIZE; ++i)
{
const u8 expected = i >= 0x17ff0 ? static_cast<u8>(i - 0x17ff0 + 1) :
i >= 0xfff0 && i < 0x10010 ? static_cast<u8>(i - 0xfff0 + 1) :
0x5a;
ASSERT_EQ(memory.Read8(cpu_address(i)), expected) << "offset=" << i << " split=" << split;
}
memory.Write32(0x0d020004, 0x0d417ff0);
memory.Write32(0x0d020008, 0x0d400020);
memory.Write32(0x0d020000, 0x80000001);
for (u32 i = 0; i < 32; ++i)
EXPECT_EQ(memory.Read8(cpu_address(0x20 + i)), i < 16 ? i + 1 : 0u);
// The CPU still sees its own split hole, not the DMA mapping.
EXPECT_EQ(memory.Read32(StarletMemory::SRAM_MIRROR_BASE + (split ? 0x8000 : 0x18000)), 0u);
}
}
TEST(StarletDMA, SHAUsesPhysicalSRAMInSplitMode)
{
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
system.GetWiiIPC().Reset();
StarletMemory memory(system);
memory.Reset();
memory.Write32(0x0d800060, 0x20);
// One padded SHA-1 block for "abc" in SRAM A.
memory.Write32(0xfffff080, 0x61626380);
memory.Write32(0xfffff0bc, 24);
constexpr std::array<u32, 5> initial = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476,
0xc3d2e1f0};
constexpr std::array<u32, 5> digest = {0xa9993e36, 0x4706816a, 0xba3e2571, 0x7850c26c,
0x9cd0d89d};
for (u32 i = 0; i < 5; ++i)
memory.Write32(0x0d030008 + i * 4, initial[i]);
memory.Write32(0x0d030004, 0x0d40f080);
memory.Write32(0x0d030000, 0x80000000);
for (u32 i = 0; i < 5; ++i)
EXPECT_EQ(memory.Read32(0x0d030008 + i * 4), digest[i]);
EXPECT_EQ(memory.Read32(0x0d030004), 0x0d40f0c0u);
}
TEST(StarletDMA, OHCIAndNANDUsePhysicalSRAMWithoutChangingCPUView)
{
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
system.GetWiiIPC().Reset();
StarletMemory memory(system);
memory.Reset();
memory.Write32(0x0d800060, 0x20);
// OHCI writes its little-endian frame number into HCCA via DMA.
memory.Write32(0x0d050018, 0x0d40f000);
memory.Write32(0x0d050004, 2U << 6);
memory.AdvanceCycles(243000);
EXPECT_EQ(memory.Read16(0xfffff080), 0x0100u);
EXPECT_EQ(memory.Read16(0x0d40f080), 0u); // CPU split hole remains a hole.
// NAND ID needs no dump and exercises the same bus-master destination mapping.
memory.Write32(0x0d010010, 0x0d40f080);
memory.Write32(0x0d010000, 0x80902005); // EXEC, READ_ID, READ, five bytes.
constexpr std::array<u8, 5> id = {0xec, 0xdc, 0x10, 0x95, 0x54};
for (u32 i = 0; i < id.size(); ++i)
EXPECT_EQ(memory.Read8(0xfffff080 + i), id[i]);
}
TEST(StarletRegisters, CachePreservesSparseValuesAndCollisions)
{
constexpr u32 address_a = 0x0d900100;
@@ -429,6 +562,54 @@ TEST(StarletRegisters, WideTimerAndInterruptAccessesMatchHardwareSemantics)
EXPECT_EQ(memory.Read32(arm_irq_mask), 0x800619efu);
}
TEST(StarletInterrupts, DriveAndResetUseDistinctHollywoodLines)
{
// Hollywood IRQ numbers, not the Broadway Processor Interface's DVD line.
EXPECT_EQ(static_cast<u32>(INT_CAUSE_DI), 1u << 18);
EXPECT_EQ(static_cast<u32>(INT_CAUSE_RST_BUTTON), 1u << 17);
EXPECT_EQ(INT_CAUSE_DI & INT_CAUSE_RST_BUTTON, 0u);
}
TEST(StarletInterrupts, DriveInterruptReachesNativeIOSMask)
{
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
auto& ipc = system.GetWiiIPC();
ipc.Reset();
StarletMemory memory(system);
memory.Reset();
constexpr u32 irq_flags = 0x0d800038;
constexpr u32 irq_mask = 0x0d80003c;
// The real IOS56 mask captured during the Shop Channel failure enables IRQ18,
// but deliberately does not enable IRQ9, where DI was previously misrouted.
constexpr u32 ios_mask = 0x800619ef;
constexpr u32 drive_irq = 1u << 18;
memory.Write32(irq_mask, ios_mask);
EXPECT_FALSE(ipc.IsStarletIRQAsserted());
for (unsigned repeat = 0; repeat < 2; ++repeat)
{
// This is the same cause and interrupt entry point used by DVDInterface.
ipc.SetStarletInterrupt(INT_CAUSE_DI, true);
EXPECT_EQ(memory.Read32(irq_flags), drive_irq);
EXPECT_TRUE(ipc.IsStarletIRQAsserted());
memory.Write32(irq_mask, ios_mask & ~drive_irq);
EXPECT_FALSE(ipc.IsStarletIRQAsserted());
memory.Write32(irq_mask, ios_mask);
EXPECT_TRUE(ipc.IsStarletIRQAsserted());
memory.Write32(irq_flags, drive_irq);
EXPECT_EQ(memory.Read32(irq_flags), 0u);
EXPECT_FALSE(ipc.IsStarletIRQAsserted());
ipc.SetStarletInterrupt(INT_CAUSE_DI, false);
}
ipc.SetStarletInterrupt(INT_CAUSE_DI, true);
ipc.SetStarletInterrupt(INT_CAUSE_DI, false);
EXPECT_EQ(memory.Read32(irq_flags), 0u);
EXPECT_FALSE(ipc.IsStarletIRQAsserted());
ipc.Reset();
}
TEST(StarletTimer, RunsAtOneTickPer128ARMCycles)
{
constexpr u32 timer = 0x0d800010;
@@ -2343,6 +2524,169 @@ TEST(StarletARMCore, ARMJitMatchesARM926UnalignedWordTransfersWithoutFallback)
#endif
}
TEST(StarletARMCore, ThumbJitKeepsAlignedBusMemoryInsideNativeBlock)
{
#if defined(_M_X86_64)
for (const bool big_endian : {false, true})
{
SCOPED_TRACE(big_endian);
TestBus interpreter_bus;
TestBus jit_bus;
ARMCore interpreter(interpreter_bus);
ARMCore jit(jit_bus);
const auto setup = [big_endian](TestBus& bus, ARMCore& core) {
bus.SetFastmemEnabled(true);
// Mixing r0/r1 cached registers, signed reads, and bus writes must retain flags and order.
bus.WriteThumb(0x00, 0x6014); // str r4, [r2]
bus.WriteThumb(0x02, 0x6810); // ldr r0, [r2]
bus.WriteThumb(0x04, 0x7054); // strb r4, [r2, #1]
bus.WriteThumb(0x06, 0x56d1); // ldrsb r1, [r2, r3]
bus.WriteThumb(0x08, 0x8054); // strh r4, [r2, #2]
bus.WriteThumb(0x0a, 0x8855); // ldrh r5, [r2, #2]
bus.WriteThumb(0x0c, 0x5ed6); // ldrsh r6, [r2, r3]
bus.WriteThumb(0x0e, 0x4050); // eor r0, r2
bus.WriteThumb(0x10, 0x9000); // str r0, [sp]
bus.WriteThumb(0x12, 0x9900); // ldr r1, [sp]
bus.WriteThumb(0x14, 0xe7fe); // b .
if (!big_endian)
{
for (u32 offset = 0; offset <= 0x14; offset += 2)
bus.WriteThumb(offset, static_cast<u16>((bus[offset + 1] << 8) | bus[offset]));
}
core.SetBigEndian(big_endian);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor) | ARMCore::CPSR_T);
core.SetRegister(2, 0x0d800000);
core.SetRegister(3, 0);
core.SetRegister(4, 0x80fe91f3);
core.SetRegister(13, 0x0d800000);
};
setup(interpreter_bus, interpreter);
setup(jit_bus, jit);
jit.SetJitEnabled(true);
EXPECT_EQ(interpreter.RunCycles(11), 11u);
EXPECT_EQ(jit.RunCycles(11), 11u);
for (u32 reg = 0; reg < 16; ++reg)
EXPECT_EQ(jit.GetRegister(reg), interpreter.GetRegister(reg)) << "r" << reg;
EXPECT_EQ(jit.GetCPSR(), interpreter.GetCPSR());
EXPECT_EQ(jit_bus.GetMMIOWord(), interpreter_bus.GetMMIOWord());
EXPECT_EQ(jit.GetJitFallbackInstructionCount(), 0u);
EXPECT_EQ(jit.GetJitNativeExecutedInstructions(), 11u);
}
#endif
}
TEST(StarletARMCore, DISABLED_ThumbBusThroughputBenchmark)
{
#if defined(_M_X86_64)
TestBus bus;
ARMCore jit(bus);
bus.SetFastmemEnabled(true);
// Exact bus reads resemble the IOS loop sampled in the Shop Channel.
bus.WriteThumb(0x00, 0x6810); // ldr r0, [r2]
bus.WriteThumb(0x02, 0x6851); // ldr r1, [r2, #4]
bus.WriteThumb(0x04, 0x6011); // str r1, [r2]
bus.WriteThumb(0x06, 0xe7fb); // b 0
bus.SetSRAMFastmemEnabled(true);
bus.SetBoot0Mapped(false);
bus.SetSRAMSplitMode(true);
bus.WriteSRAM32(0xf000, 0x12345678);
bus.WriteSRAM32(0xf004, 0x87654321);
jit.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor) | ARMCore::CPSR_T);
jit.SetRegister(2, 0xfffff000);
jit.SetJitEnabled(true);
constexpr u64 cycles = 4'000'000;
const auto start = std::chrono::steady_clock::now();
EXPECT_EQ(jit.RunCycles(cycles), cycles);
const auto us = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - start)
.count();
EXPECT_EQ(jit.GetRegister(0), 0x87654321u);
EXPECT_EQ(jit.GetRegister(1), 0x87654321u);
std::cout << "Thumb exact-bus loop: " << us << " us for " << cycles
<< " instructions; fallbacks=" << jit.GetJitFallbackInstructionCount() << '\n';
#endif
}
TEST(StarletARMCore, ThumbJitPreservesUnalignedMemorySemantics)
{
#if defined(_M_X86_64)
for (const bool big_endian : {false, true})
{
SCOPED_TRACE(big_endian);
TestBus interpreter_bus(0x2000);
TestBus jit_bus(0x2000);
ARMCore interpreter(interpreter_bus);
ARMCore jit(jit_bus);
const auto setup = [big_endian](TestBus& bus, ARMCore& core) {
bus.SetFastmemEnabled(true);
const std::array<u16, 7> code{0x58d0, 0x6811, 0x5ed4, 0x50d5, 0x52d6, 0x6017, 0xe7fe};
// Register-offset LDR rotates an aligned word; immediate LDR and odd halfwords retain
// the interpreter's bytewise path, including accesses crossing the 1 KiB TLB boundary.
for (u32 i = 0; i < code.size(); ++i)
{
const u16 op = code[i];
bus.WriteThumb(0x1000 + i * 2, big_endian ? op : static_cast<u16>((op >> 8) | (op << 8)));
}
bus.WriteARM(0x3fc, 0x81fe9273);
bus.WriteARM(0x400, 0xa5b6c7d8);
core.Reset(0x1000);
core.SetBigEndian(big_endian);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor) | ARMCore::CPSR_T);
core.SetRegister(2, 0x3ff);
core.SetRegister(3, 0);
core.SetRegister(5, 0x12345678);
core.SetRegister(6, 0x89ab);
core.SetRegister(7, 0xc0ffee01);
};
setup(interpreter_bus, interpreter);
setup(jit_bus, jit);
jit.SetJitEnabled(true);
EXPECT_EQ(interpreter.RunCycles(7), 7u);
EXPECT_EQ(jit.RunCycles(7), 7u);
for (u32 reg = 0; reg < 16; ++reg)
EXPECT_EQ(jit.GetRegister(reg), interpreter.GetRegister(reg)) << "r" << reg;
EXPECT_EQ(jit.GetCPSR(), interpreter.GetCPSR());
for (u32 address = 0x3fc; address < 0x408; ++address)
EXPECT_EQ(jit_bus[address], interpreter_bus[address]);
EXPECT_EQ(jit.GetJitFallbackInstructionCount(), 6u);
}
#endif
}
TEST(StarletARMCore, ThumbJitBusMemoryPreservesBoot0AndSRAMHoles)
{
#if defined(_M_X86_64)
for (const bool split : {false, true})
{
for (const u32 address : {split ? 0xfffe0000u : 0xffff0000u, split ? 0xfffe9000u : 0xffff9000u})
{
SCOPED_TRACE(address);
TestBus bus;
ARMCore core(bus);
bus.SetFastmemEnabled(true);
bus.SetSRAMFastmemEnabled(true);
bus.SetBoot0Mapped(true);
bus.SetSRAMSplitMode(split);
bus.WriteThumb(0, 0x6014); // str r4, [r2]
bus.WriteThumb(2, 0x6810); // ldr r0, [r2]
bus.WriteThumb(4, 0xe7fe);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor) | ARMCore::CPSR_T);
core.SetRegister(2, address);
core.SetRegister(4, 0x12345678);
core.SetJitEnabled(true);
ASSERT_EQ(core.RunCycles(3), 3u);
EXPECT_EQ(core.GetRegister(0), 0u);
EXPECT_EQ(core.GetJitFallbackInstructionCount(), 0u);
EXPECT_EQ(core.GetJitSlowSRAMAccessCount(), 2u);
EXPECT_TRUE(bus.SRAMCanariesIntact());
// TestBus initializes both SRAM banks and guards with the same canary byte.
for (u32 offset = 0; offset < 0x18000; offset += 4)
EXPECT_EQ(bus.ReadSRAM32(offset), 0xa5a5a5a5u);
}
}
#endif
}
TEST(StarletARMCore, ARMJitUsesReadFastmemForProfiledSplitSRAMMirrorPages)
{
#if defined(_M_X86_64)
@@ -0,0 +1,600 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <vector>
#include <gtest/gtest.h>
#ifdef _WIN32
#include <winsock2.h>
#else
#include <arpa/inet.h>
#include <poll.h>
#endif
#define LIBSLIRP_STATIC
#include "libslirp.h"
#include "Common/ChunkFile.h"
#include "Common/FileUtil.h"
#include "Common/Network.h"
#include "Common/ScopeGuard.h"
#include "Core/Core.h"
#include "Core/IOS/Starlet/AX88772.h"
#include "Core/IOS/Starlet/SlirpNetwork.h"
#include "Core/IOS/Starlet/StarletMemory.h"
#include "Core/System.h"
using IOS::LLE::AX88772;
using Result = AX88772::Result;
namespace
{
std::array<u8, 8> USBSetup(u8 type, u8 request, u16 value, u16 index, u16 length)
{
return {type,
request,
static_cast<u8>(value),
static_cast<u8>(value >> 8),
static_cast<u8>(index),
static_cast<u8>(index >> 8),
static_cast<u8>(length),
static_cast<u8>(length >> 8)};
}
std::vector<u8> Framed(std::span<const u8> frame)
{
const u16 length = static_cast<u16>(frame.size());
std::vector<u8> result{static_cast<u8>(length), static_cast<u8>(length >> 8),
static_cast<u8>(~length), static_cast<u8>(~length >> 8)};
result.insert(result.end(), frame.begin(), frame.end());
return result;
}
std::vector<u8> Control(AX88772& device, u8 type, u8 request, u16 value, u16 index,
std::vector<u8> data = {})
{
auto setup = USBSetup(type, request, value, index, static_cast<u16>(data.size()));
size_t actual = 0;
EXPECT_EQ(device.Transfer(0, 0, setup, &actual), Result::Completed);
EXPECT_EQ(actual, 8u);
if (!data.empty())
{
EXPECT_EQ(device.Transfer(0, (type & 0x80) ? 2 : 1, data, &actual), Result::Completed);
data.resize(actual);
}
EXPECT_EQ(device.Transfer(0, (type & 0x80) ? 1 : 2, {}, &actual), Result::Completed);
return data;
}
void Configure(AX88772& device)
{
Control(device, 0, 9, 1, 0);
Control(device, 0x40, 0x10, 0x88, 0);
Control(device, 0x40, 0x1b, 0x306, 0);
device.SetLink(true);
}
std::string RuntimeDirectory()
{
return File::GetExeDirectory() + "/../Network";
}
} // namespace
TEST(StarletAX88772, EnumeratesSupportedVIDPIDAndFullSpeedEndpoints)
{
AX88772 device({});
const auto descriptor = Control(device, 0x80, 6, 0x100, 0, std::vector<u8>(64));
ASSERT_EQ(descriptor.size(), 18u);
EXPECT_EQ(descriptor[8], 0x95);
EXPECT_EQ(descriptor[9], 0x0b);
EXPECT_EQ(descriptor[10], 0x20);
EXPECT_EQ(descriptor[11], 0x77);
const auto config = Control(device, 0x80, 6, 0x200, 0, std::vector<u8>(255));
ASSERT_EQ(config.size(), 39u);
EXPECT_EQ(config[20], 0x81);
EXPECT_EQ(config[27], 0x82);
EXPECT_EQ(config[29], 64);
EXPECT_EQ(config[34], 3);
}
TEST(StarletAX88772, AddressChangesOnlyAfterStatusStage)
{
AX88772 device({});
size_t actual;
auto setup = USBSetup(0, 5, 7, 0, 0);
ASSERT_EQ(device.Transfer(0, 0, setup, &actual), Result::Completed);
EXPECT_EQ(device.GetAddress(), 0);
ASSERT_EQ(device.Transfer(0, 2, {}, &actual), Result::Completed);
EXPECT_EQ(device.GetAddress(), 7);
device.Reset();
EXPECT_EQ(device.GetAddress(), 0);
}
TEST(StarletAX88772, SplitControlReadsAdvanceRatherThanRepeat)
{
AX88772 device({});
size_t actual;
auto setup = USBSetup(0x80, 6, 0x100, 0, 18);
ASSERT_EQ(device.Transfer(0, 0, setup, &actual), Result::Completed);
std::array<u8, 8> first{};
std::array<u8, 10> second{};
ASSERT_EQ(device.Transfer(0, 2, first, &actual), Result::Completed);
EXPECT_EQ(actual, 8u);
ASSERT_EQ(device.Transfer(0, 2, second, &actual), Result::Completed);
EXPECT_EQ(actual, 10u);
EXPECT_EQ(second[0], 0x95);
}
TEST(StarletAX88772, UnknownAndMalformedRequestsStall)
{
AX88772 device({});
size_t actual;
for (auto setup : {USBSetup(0xc0, 0xff, 0, 0, 2), USBSetup(0x40, 8, 0x10, 0, 3),
USBSetup(0x40, 0x14, 0, 0, 7), USBSetup(0x80, 6, 0x3ff, 0, 10)})
EXPECT_EQ(device.Transfer(0, 0, setup, &actual), Result::Stalled);
}
TEST(StarletAX88772, MACAndMIIRegistersRoundTrip)
{
AX88772 device({});
const std::vector<u8> mac{2, 3, 4, 5, 6, 7};
Control(device, 0x40, 0x14, 0, 0, mac);
EXPECT_EQ(Control(device, 0xc0, 0x13, 0, 0, std::vector<u8>(6)), mac);
Control(device, 0x40, 0x08, 0x10, 4, {0xe1, 1});
EXPECT_EQ(Control(device, 0xc0, 0x07, 0x10, 4, std::vector<u8>(2)), (std::vector<u8>{0xe1, 1}));
device.SetLink(true);
const auto status = Control(device, 0xc0, 0x07, 0x10, 1, std::vector<u8>(2));
EXPECT_EQ(status[0] & 0x24, 0x24);
}
TEST(StarletAX88772, LinkChangesRefreshStatusWithoutWaitingForPeriodicReport)
{
AX88772 device({});
Configure(device);
std::array<u8, 8> event{};
size_t actual;
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed);
EXPECT_EQ(event[2], 1);
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending);
device.SetLink(false);
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed);
EXPECT_EQ(event[2], 0);
}
TEST(StarletAX88772, UnchangedLinkGetsPeriodicStatusWithoutBusyPollingOrBacklog)
{
AX88772 device({});
Configure(device);
const auto config = Control(device, 0x80, 6, 0x200, 0, std::vector<u8>(255));
ASSERT_EQ(config.size(), 39u);
const unsigned interval = config[24];
ASSERT_EQ(interval, 10u);
std::array<u8, 8> event{};
size_t actual = 0;
for (unsigned report = 0; report < 3; ++report)
{
ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed);
EXPECT_EQ(actual, 8u);
EXPECT_EQ(event[2], 1);
for (unsigned frame = 0; frame < interval; ++frame)
{
for (unsigned poll = 0; poll < 3; ++poll)
{
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending);
EXPECT_EQ(actual, 0u);
}
device.AdvanceUSBFrame();
}
}
// Leaving the endpoint unpolled coalesces reports; it does not queue them.
for (unsigned frame = 0; frame < 1000; ++frame)
device.AdvanceUSBFrame();
ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed);
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending);
}
TEST(StarletAX88772, PeriodicStatusWaitsForHardwareMDIOOwnership)
{
AX88772 device({});
Configure(device);
std::array<u8, 8> event{};
size_t actual = 0;
ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed);
Control(device, 0x40, 0x06, 0, 0);
for (unsigned frame = 0; frame < 20; ++frame)
device.AdvanceUSBFrame();
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending);
Control(device, 0x40, 0x0a, 0, 0);
ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed);
EXPECT_EQ(event, (std::array<u8, 8>{0xa1, 0, 1, 0, 0x2d, 0x78, 0xe1, 0x45}));
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending);
}
TEST(StarletAX88772, StateRoundTripPreservesPeriodicStatusPhaseAndResetClearsIt)
{
AX88772 device({});
Configure(device);
std::array<u8, 8> event{};
size_t actual = 0;
ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed);
for (unsigned frame = 0; frame < 4; ++frame)
device.AdvanceUSBFrame();
std::vector<u8> state(65536);
u8* ptr = state.data();
PointerWrap writer(&ptr, state.size(), PointerWrap::Mode::Write);
device.DoState(writer);
ASSERT_TRUE(writer.IsWriteMode());
const size_t size = ptr - state.data();
AX88772 restored({});
ptr = state.data();
PointerWrap reader(&ptr, size, PointerWrap::Mode::Read);
restored.DoState(reader);
ASSERT_TRUE(reader.IsReadMode());
for (unsigned frame = 0; frame < 6; ++frame)
{
EXPECT_EQ(restored.Transfer(1, 2, event, &actual), Result::Pending);
restored.AdvanceUSBFrame();
}
ASSERT_EQ(restored.Transfer(1, 2, event, &actual), Result::Completed);
EXPECT_EQ(event[2], 1);
restored.Reset();
EXPECT_EQ(restored.Transfer(1, 2, event, &actual), Result::Stalled);
Configure(restored);
EXPECT_EQ(restored.Transfer(1, 2, event, &actual), Result::Completed);
}
TEST(StarletAX88772, EEPROMAndPHYAddressRegisterDescribeTheSameHardware)
{
AX88772 device({});
const auto eeprom = Control(device, 0xc0, 0x0b, 0x11, 0, std::vector<u8>(2));
EXPECT_EQ(eeprom, (std::vector<u8>{0x10, 0xe0}));
EXPECT_EQ(Control(device, 0xc0, 0x19, 0, 0, std::vector<u8>(2)), (std::vector<u8>{0xe0, 0x10}));
// Accessing an absent PHY must not poison the USB control endpoint.
Control(device, 0x40, 0x08, 0xff, 4, {0, 0});
EXPECT_EQ(Control(device, 0xc0, 0x07, 0xff, 4, std::vector<u8>(2)),
(std::vector<u8>{0xff, 0xff}));
// Only the low five bits reach MDIO, as specified by the device datasheet.
Control(device, 0x40, 0x08, 0xf0, 0x24, {0x23, 1});
EXPECT_EQ(Control(device, 0xc0, 0x07, 0x10, 4, std::vector<u8>(2)), (std::vector<u8>{0x23, 1}));
}
TEST(StarletAX88772, InterruptIncludesConfiguredPHYRegistersAndWaitsForMDIOOwnership)
{
AX88772 device({});
Configure(device);
EXPECT_EQ(Control(device, 0xc0, 0x0b, 0x0f, 0, std::vector<u8>(2)), (std::vector<u8>{5, 1}));
EXPECT_EQ(Control(device, 0xc0, 0x0b, 0x10, 0, std::vector<u8>(2)), (std::vector<u8>{0xee, 5}));
Control(device, 0x40, 0x06, 0, 0);
std::array<u8, 8> event{};
size_t actual;
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending);
Control(device, 0x40, 0x0a, 0, 0);
ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed);
EXPECT_EQ(event, (std::array<u8, 8>{0xa1, 0, 1, 0, 0x2d, 0x78, 0xe1, 0x45}));
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending);
}
TEST(StarletAX88772, TransmitsOddLengthFramesAcrossUSBTransfers)
{
std::vector<std::vector<u8>> received;
AX88772 device(
[&](std::span<const u8> frame) { received.emplace_back(frame.begin(), frame.end()); });
Configure(device);
std::vector<u8> frame(61, 0x42);
auto packet = Framed(frame);
size_t actual;
EXPECT_EQ(device.Transfer(3, 1, std::span(packet).first(17), &actual), Result::Completed);
EXPECT_TRUE(received.empty());
EXPECT_EQ(device.Transfer(3, 1, std::span(packet).subspan(17), &actual), Result::Completed);
ASSERT_EQ(received.size(), 1u);
EXPECT_EQ(received[0], frame);
packet[2] ^= 1;
EXPECT_EQ(device.Transfer(3, 1, packet, &actual), Result::Stalled);
EXPECT_EQ(received.size(), 1u);
}
TEST(StarletAX88772, ReceiveFramingAndResetPreservePacketBoundaries)
{
AX88772 device({});
Configure(device);
std::vector<u8> frame(61, 0x42);
std::fill_n(frame.begin(), 6, 0xff);
device.ReceiveFrame(frame);
std::array<u8, 2048> buffer{};
size_t actual;
ASSERT_EQ(device.Transfer(2, 2, buffer, &actual), Result::Completed);
EXPECT_EQ(actual, 66u);
EXPECT_EQ(buffer[0], 61);
EXPECT_EQ(buffer[2], static_cast<u8>(~61));
EXPECT_TRUE(std::equal(frame.begin(), frame.end(), buffer.begin() + 4));
EXPECT_EQ(device.Transfer(2, 2, buffer, &actual), Result::Pending);
device.ReceiveFrame(frame);
device.Reset();
EXPECT_EQ(device.Transfer(2, 2, buffer, &actual), Result::Stalled);
Configure(device);
EXPECT_EQ(device.Transfer(2, 2, buffer, &actual), Result::Pending);
}
TEST(StarletEthernetNAT, PollEventMappingPreservesReadWritePriorityAndErrors)
{
using namespace IOS::LLE::SlirpSocketPolling;
for (int flags = 0; flags < 8; ++flags)
EXPECT_EQ(ToSlirpEvents(ToNativeEvents(flags)), flags);
EXPECT_EQ(ToSlirpEvents(POLLERR | POLLHUP), SLIRP_POLL_ERR | SLIRP_POLL_HUP);
EXPECT_EQ(ToSlirpEvents(POLLNVAL), SLIRP_POLL_ERR);
#ifdef _WIN32
EXPECT_EQ(ToNativeEvents(SLIRP_POLL_IN), POLLRDNORM);
EXPECT_EQ(ToNativeEvents(SLIRP_POLL_PRI), POLLRDBAND);
EXPECT_EQ(ToNativeEvents(SLIRP_POLL_IN | SLIRP_POLL_PRI) & POLLPRI, 0);
#endif
}
#ifdef _WIN32
TEST(StarletEthernetNAT, WinsockTCPReadAndPeerCloseWorkWithSlirpRequestedEvents)
{
using namespace IOS::LLE::SlirpSocketPolling;
WSADATA wsadata{};
ASSERT_EQ(WSAStartup(MAKEWORD(2, 2), &wsadata), 0);
Common::ScopeGuard cleanup([] { WSACleanup(); });
const SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
ASSERT_NE(listener, INVALID_SOCKET);
Common::ScopeGuard close_listener([&] { closesocket(listener); });
sockaddr_in address{};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
ASSERT_EQ(bind(listener, reinterpret_cast<const sockaddr*>(&address), sizeof(address)), 0);
ASSERT_EQ(listen(listener, 1), 0);
int address_size = sizeof(address);
ASSERT_EQ(getsockname(listener, reinterpret_cast<sockaddr*>(&address), &address_size), 0);
const SOCKET client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
ASSERT_NE(client, INVALID_SOCKET);
Common::ScopeGuard close_client([&] { closesocket(client); });
ASSERT_EQ(connect(client, reinterpret_cast<const sockaddr*>(&address), sizeof(address)), 0);
u_long nonblocking = 1;
ASSERT_EQ(ioctlsocket(client, FIONBIO, &nonblocking), 0);
const SOCKET server = accept(listener, nullptr, nullptr);
ASSERT_NE(server, INVALID_SOCKET);
Common::ScopeGuard close_server([&] { closesocket(server); });
ASSERT_EQ(send(server, "test", 4, 0), 4);
ASSERT_EQ(shutdown(server, SD_SEND), 0);
// libslirp adds PRI immediately after a TCP connection is established. An
// unsupported Winsock event here used to prevent receiving any data or FIN.
WSAPOLLFD fd{client, ToNativeEvents(SLIRP_POLL_IN | SLIRP_POLL_PRI), 0};
ASSERT_EQ(WSAPoll(&fd, 1, 1000), 1) << WSAGetLastError();
EXPECT_NE(ToSlirpEvents(fd.revents) & SLIRP_POLL_IN, 0);
std::array<char, 4> data{};
ASSERT_EQ(recv(client, data.data(), static_cast<int>(data.size()), 0), 4);
EXPECT_EQ(data, (std::array<char, 4>{'t', 'e', 's', 't'}));
fd.revents = 0;
ASSERT_EQ(WSAPoll(&fd, 1, 1000), 1) << WSAGetLastError();
EXPECT_NE(ToSlirpEvents(fd.revents) & (SLIRP_POLL_IN | SLIRP_POLL_HUP), 0);
EXPECT_EQ(recv(client, data.data(), static_cast<int>(data.size()), 0), 0);
}
#endif
TEST(StarletEthernetNAT, MissingRuntimeFailsExplicitly)
{
IOS::LLE::SlirpNetwork network;
std::string error;
EXPECT_FALSE(network.Start("/nonexistent-dolphin-slirp-runtime", {}, &error));
EXPECT_FALSE(error.empty());
}
TEST(StarletEthernetNAT, LibslirpAnswersARPThroughAX88772BulkEndpoints)
{
if (!File::IsDirectory(RuntimeDirectory()))
GTEST_SKIP() << "Optional local libslirp runtime absent";
IOS::LLE::SlirpNetwork network;
AX88772 device([&](std::span<const u8> frame) { network.Input(frame); });
std::string error;
ASSERT_TRUE(network.Start(
RuntimeDirectory(), [&](std::span<const u8> frame) { device.ReceiveFrame(frame); }, &error))
<< error;
Configure(device);
Common::MACAddress mac{2, 0x44, 0x4f, 0x4c, 0, 1};
Common::ARPPacket request(Common::MACAddress{255, 255, 255, 255, 255, 255}, mac);
request.arp_header.opcode = htons(1);
request.arp_header.sender_address = mac;
request.arp_header.sender_ip = htonl(0x0a00020f);
request.arp_header.target_ip = htonl(0x0a000202);
auto framed = Framed(request.Build());
size_t actual;
ASSERT_EQ(device.Transfer(3, 1, framed, &actual), Result::Completed);
network.Poll(243000);
std::array<u8, 2048> buffer{};
ASSERT_EQ(device.Transfer(2, 2, buffer, &actual), Result::Completed);
ASSERT_GE(actual, 46u);
const auto arp =
Common::PacketView(buffer.data() + 4, buffer[0] | (buffer[1] << 8)).GetARPPacket();
ASSERT_TRUE(arp.has_value());
EXPECT_EQ(ntohs(arp->arp_header.opcode), 2);
EXPECT_EQ(arp->arp_header.sender_ip, htonl(0x0a000202));
}
TEST(StarletEthernetNAT, LibslirpDHCPProvidesAddressRouterAndDNS)
{
if (!File::IsDirectory(RuntimeDirectory()))
GTEST_SKIP() << "Optional local libslirp runtime absent";
IOS::LLE::SlirpNetwork network;
std::vector<std::vector<u8>> replies;
std::string error;
ASSERT_TRUE(network.Start(
RuntimeDirectory(),
[&](std::span<const u8> frame) { replies.emplace_back(frame.begin(), frame.end()); }, &error))
<< error;
Common::MACAddress mac{2, 0x44, 0x4f, 0x4c, 0, 1};
Common::DHCPPacket discover;
discover.body.message_type = 1;
discover.body.hardware_type = 1;
discover.body.hardware_addr = 6;
discover.body.transaction_id = htonl(0x12345678);
discover.body.boot_flag = htons(0x8000);
discover.body.client_mac = mac;
discover.AddOption(53, {1});
discover.AddOption(55, {1, 3, 6});
sockaddr_in from{}, to{};
from.sin_port = htons(68);
to.sin_addr.s_addr = 0xffffffff;
to.sin_port = htons(67);
Common::UDPPacket packet(Common::MACAddress{255, 255, 255, 255, 255, 255}, mac, from, to,
discover.Build());
network.Input(packet.Build());
network.Poll(243000);
ASSERT_FALSE(replies.empty());
const auto udp = Common::PacketView(replies.back().data(), replies.back().size()).GetUDPPacket();
ASSERT_TRUE(udp.has_value());
ASSERT_GE(udp->data.size(), Common::DHCPBody::SIZE);
Common::DHCPPacket offer(udp->data);
EXPECT_EQ(offer.body.transaction_id, discover.body.transaction_id);
EXPECT_EQ(offer.body.your_ip, htonl(0x0a00020f));
EXPECT_TRUE(std::ranges::any_of(offer.options, [](const auto& option) {
return option == std::vector<u8>{3, 4, 10, 0, 2, 2};
}));
EXPECT_TRUE(std::ranges::any_of(offer.options, [](const auto& option) {
return option == std::vector<u8>{6, 4, 10, 0, 2, 3};
}));
}
TEST(StarletEthernetNAT, ExternalOHCIEnumeratesDeviceWithoutTouchingBluetooth)
{
if (!File::IsDirectory(RuntimeDirectory()))
GTEST_SKIP() << "Optional local libslirp runtime absent";
Core::DeclareAsCPUThread();
IOS::LLE::StarletMemory memory(Core::System::GetInstance());
memory.Reset();
std::string error;
ASSERT_TRUE(memory.EnableEthernetNAT(RuntimeDirectory(), &error)) << error;
constexpr u32 ohci = 0x0d050000, hcca = 0x0d404000, ed = hcca + 0x100;
constexpr u32 setup_td = hcca + 0x200, data_td = hcca + 0x210, status_td = hcca + 0x220,
tail = hcca + 0x230;
constexpr u32 setup_buffer = hcca + 0x300, data_buffer = hcca + 0x400;
const auto put = [&](u32 addr, u32 value) {
for (u32 i = 0; i < 4; ++i)
memory.Write8(addr + i, static_cast<u8>(value >> (8 * i)));
};
const auto get = [&](u32 addr) {
u32 value = 0;
for (u32 i = 0; i < 4; ++i)
value |= u32(memory.Read8(addr + i)) << (8 * i);
return value;
};
EXPECT_NE(memory.Read32(ohci + 0x54) & 1, 0u);
const u32 bluetooth_before = memory.Read32(0x0d060054);
memory.Write32(ohci + 0x54, 2); // Enable connected port.
const auto setup = USBSetup(0x80, 6, 0x100, 0, 18);
for (u32 i = 0; i < 8; ++i)
memory.Write8(setup_buffer + i, setup[i]);
put(ed, 64u << 16);
put(ed + 4, tail);
put(ed + 8, setup_td);
put(ed + 12, 0);
put(setup_td, 0);
put(setup_td + 4, setup_buffer);
put(setup_td + 8, data_td);
put(setup_td + 12, setup_buffer + 7);
put(data_td, 2u << 19);
put(data_td + 4, data_buffer);
put(data_td + 8, status_td);
put(data_td + 12, data_buffer + 17);
put(status_td, 1u << 19);
put(status_td + 4, 0);
put(status_td + 8, tail);
put(status_td + 12, 0);
memory.Write32(ohci + 0x18, hcca);
memory.Write32(ohci + 0x20, ed);
memory.Write32(ohci + 4, 0x90); // Operational + control list.
EXPECT_EQ(get(ed + 8) & ~0xfu, tail);
EXPECT_EQ(memory.Read8(data_buffer + 8), 0x95);
EXPECT_EQ(memory.Read8(data_buffer + 10), 0x20);
EXPECT_EQ(memory.Read32(0x0d060054), bluetooth_before);
}
TEST(StarletEthernetNAT, ExternalOHCICompletesRepeatedStatusReadsWithoutLinkChanges)
{
if (!File::IsDirectory(RuntimeDirectory()))
GTEST_SKIP() << "Optional local libslirp runtime absent";
Core::DeclareAsCPUThread();
IOS::LLE::StarletMemory memory(Core::System::GetInstance());
memory.Reset();
std::string error;
ASSERT_TRUE(memory.EnableEthernetNAT(RuntimeDirectory(), &error)) << error;
constexpr u32 ohci = 0x0d050000, hcca = 0x0d404000, ed = hcca + 0x100;
constexpr u32 td = hcca + 0x200, status_td = td + 0x10, tail = td + 0x20;
constexpr u32 buffer = hcca + 0x300;
const auto put = [&](u32 addr, u32 value) {
for (u32 i = 0; i < 4; ++i)
memory.Write8(addr + i, static_cast<u8>(value >> (8 * i)));
};
const auto get = [&](u32 addr) {
u32 value = 0;
for (u32 i = 0; i < 4; ++i)
value |= u32(memory.Read8(addr + i)) << (8 * i);
return value;
};
// Configure through endpoint zero, then submit the same synchronous status
// reads used by IOS's Ethernet link polling thread. No guest code is bypassed.
memory.Write32(ohci + 0x54, 2);
const auto setup = USBSetup(0, 9, 1, 0, 0);
for (u32 i = 0; i < 8; ++i)
memory.Write8(buffer + i, setup[i]);
put(ed, 64u << 16);
put(ed + 4, tail);
put(ed + 8, td);
put(td, 0);
put(td + 4, buffer);
put(td + 8, status_td);
put(td + 12, buffer + 7);
put(status_td, 2u << 19);
put(status_td + 8, tail);
memory.Write32(ohci + 0x18, hcca);
memory.Write32(ohci + 0x20, ed);
memory.Write32(ohci + 4, 0x90);
ASSERT_EQ(get(ed + 8) & ~0xfu, tail);
memory.Write32(ohci + 0x0c, 2); // Acknowledge the configuration completion.
for (u32 slot = 0; slot < 32; ++slot)
put(hcca + slot * 4, ed);
put(ed, (8u << 16) | (2u << 11) | (1u << 7));
const auto rearm = [&] {
put(td, 0xf0000000u | (2u << 19));
put(td + 4, buffer);
put(td + 8, tail);
put(td + 12, buffer + 7);
put(ed + 8, td);
};
rearm();
memory.Write32(ohci + 4, 0x84); // Operational + periodic list.
ASSERT_EQ(get(ed + 8) & ~0xfu, tail);
EXPECT_EQ(memory.Read8(buffer + 2), 1);
memory.Write32(ohci + 0x0c, 2);
rearm();
constexpr u64 cycles_per_ms = 243000;
memory.AdvanceCycles(9 * cycles_per_ms);
EXPECT_EQ(get(ed + 8) & ~0xfu, td);
memory.AdvanceCycles(cycles_per_ms);
EXPECT_EQ(get(ed + 8) & ~0xfu, tail);
EXPECT_EQ(get(hcca + 0x84), td);
EXPECT_NE(memory.Read32(ohci + 0x0c) & 2, 0u);
EXPECT_EQ(memory.Read8(buffer + 2), 1);
}
TEST(StarletEthernetNAT, EHCIHandsFullSpeedDeviceBackToExternalOHCI)
{
if (!File::IsDirectory(RuntimeDirectory()))
GTEST_SKIP() << "Optional local libslirp runtime absent";
Core::DeclareAsCPUThread();
IOS::LLE::StarletMemory memory(Core::System::GetInstance());
memory.Reset();
std::string error;
ASSERT_TRUE(memory.EnableEthernetNAT(RuntimeDirectory(), &error)) << error;
constexpr u32 ehci = 0x0d040000, ohci_port = 0x0d050054;
EXPECT_NE(memory.Read32(ohci_port) & 1, 0u);
memory.Write32(ehci + 0x50, 1); // CONFIGFLAG claims the external ports.
EXPECT_EQ(memory.Read32(ohci_port) & 3, 0u);
EXPECT_NE(memory.Read32(ehci + 0x54) & 1, 0u);
memory.Write32(ehci + 0x54, 0x3000); // Port power + companion owner.
EXPECT_NE(memory.Read32(ohci_port) & 1, 0u);
memory.Write32(ehci + 0x54, 0x2000); // Power off.
EXPECT_EQ(memory.Read32(ohci_port) & 3, 0u);
memory.Write32(ehci + 0x54, 0x1100); // Powered reset detects full speed.
EXPECT_NE(memory.Read32(ehci + 0x54) & 0x2000, 0u);
EXPECT_NE(memory.Read32(ohci_port) & 1, 0u);
}