Files
Yaya48 b931671fde 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.
2026-09-12 11:08:25 +02:00

601 lines
22 KiB
C++

// 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);
}