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:
@@ -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