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:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user