// Copyright 2026 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #ifdef _WIN32 #include #else #include #include #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 USBSetup(u8 type, u8 request, u16 value, u16 index, u16 length) { return {type, request, static_cast(value), static_cast(value >> 8), static_cast(index), static_cast(index >> 8), static_cast(length), static_cast(length >> 8)}; } std::vector Framed(std::span frame) { const u16 length = static_cast(frame.size()); std::vector result{static_cast(length), static_cast(length >> 8), static_cast(~length), static_cast(~length >> 8)}; result.insert(result.end(), frame.begin(), frame.end()); return result; } std::vector Control(AX88772& device, u8 type, u8 request, u16 value, u16 index, std::vector data = {}) { auto setup = USBSetup(type, request, value, index, static_cast(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(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(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 first{}; std::array 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 mac{2, 3, 4, 5, 6, 7}; Control(device, 0x40, 0x14, 0, 0, mac); EXPECT_EQ(Control(device, 0xc0, 0x13, 0, 0, std::vector(6)), mac); Control(device, 0x40, 0x08, 0x10, 4, {0xe1, 1}); EXPECT_EQ(Control(device, 0xc0, 0x07, 0x10, 4, std::vector(2)), (std::vector{0xe1, 1})); device.SetLink(true); const auto status = Control(device, 0xc0, 0x07, 0x10, 1, std::vector(2)); EXPECT_EQ(status[0] & 0x24, 0x24); } TEST(StarletAX88772, LinkChangesRefreshStatusWithoutWaitingForPeriodicReport) { AX88772 device({}); Configure(device); std::array 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(255)); ASSERT_EQ(config.size(), 39u); const unsigned interval = config[24]; ASSERT_EQ(interval, 10u); std::array 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 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{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 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 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(2)); EXPECT_EQ(eeprom, (std::vector{0x10, 0xe0})); EXPECT_EQ(Control(device, 0xc0, 0x19, 0, 0, std::vector(2)), (std::vector{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(2)), (std::vector{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(2)), (std::vector{0x23, 1})); } TEST(StarletAX88772, InterruptIncludesConfiguredPHYRegistersAndWaitsForMDIOOwnership) { AX88772 device({}); Configure(device); EXPECT_EQ(Control(device, 0xc0, 0x0b, 0x0f, 0, std::vector(2)), (std::vector{5, 1})); EXPECT_EQ(Control(device, 0xc0, 0x0b, 0x10, 0, std::vector(2)), (std::vector{0xee, 5})); Control(device, 0x40, 0x06, 0, 0); std::array 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{0xa1, 0, 1, 0, 0x2d, 0x78, 0xe1, 0x45})); EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending); } TEST(StarletAX88772, TransmitsOddLengthFramesAcrossUSBTransfers) { std::vector> received; AX88772 device( [&](std::span frame) { received.emplace_back(frame.begin(), frame.end()); }); Configure(device); std::vector 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 frame(61, 0x42); std::fill_n(frame.begin(), 6, 0xff); device.ReceiveFrame(frame); std::array 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(~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(&address), sizeof(address)), 0); ASSERT_EQ(listen(listener, 1), 0); int address_size = sizeof(address); ASSERT_EQ(getsockname(listener, reinterpret_cast(&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(&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 data{}; ASSERT_EQ(recv(client, data.data(), static_cast(data.size()), 0), 4); EXPECT_EQ(data, (std::array{'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(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 frame) { network.Input(frame); }); std::string error; ASSERT_TRUE(network.Start( RuntimeDirectory(), [&](std::span 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 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> replies; std::string error; ASSERT_TRUE(network.Start( RuntimeDirectory(), [&](std::span 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{3, 4, 10, 0, 2, 2}; })); EXPECT_TRUE(std::ranges::any_of(offer.options, [](const auto& option) { return option == std::vector{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(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(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); }