IOS: checkpoint native Wii Shop connectivity and Starlet optimizations

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

Validated: 164 targeted tests pass. User confirmed Wii Shop connection and channel-list navigation at 100% speed / 59.96 FPS on 2026-09-12. Downloads and general channel performance remain unvalidated; local firmware, keys and runtime data are excluded.
This commit is contained in:
2026-09-12 11:08:25 +02:00
parent ebb753d09a
commit b931671fde
24 changed files with 2952 additions and 64 deletions
+33
View File
@@ -0,0 +1,33 @@
Slirp was written by Danny Gasparovski.
Copyright (c), 1995,1996 All Rights Reserved.
Slirp is free software; "free" as in you don't have to pay for it, and you
are free to do whatever you want with it. I do not accept any donations,
monetary or otherwise, for Slirp. Instead, I would ask you to pass this
potential donation to your favorite charity. In fact, I encourage
*everyone* who finds Slirp useful to make a small donation to their
favorite charity (for example, GreenPeace). This is not a requirement, but
a suggestion from someone who highly values the service they provide.
See LICENSE for the full copyright terms and conditions.
They basically mean you can do anything you want with the software, except
1) call it your own, and 2) claim warranty on it. There is no warranty for
this software. None. Nada. If you lose a million dollars while using
Slirp, that's your loss not mine. So, ***USE AT YOUR OWN RISK!***.
If these conditions cannot be met due to legal restrictions (E.g. where it
is against the law to give out Software without warranty), you must cease
using the software and delete all copies you have.
Slirp uses code that is copyrighted by the following people/organizations:
Juha Pirkola.
Gregory M. Christy.
The Regents of the University of California.
Carnegie Mellon University.
The Australian National University.
RSA Data Security, Inc.
Please read the top of each source file for the details on the various
copyrights.
+24
View File
@@ -0,0 +1,24 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#ifndef LIBSLIRP_VERSION_H_
#define LIBSLIRP_VERSION_H_
#ifdef __cplusplus
extern "C" {
#endif
#define SLIRP_MAJOR_VERSION 4
#define SLIRP_MINOR_VERSION 9
#define SLIRP_MICRO_VERSION 3
#define SLIRP_VERSION_STRING "4.9.3"
#define SLIRP_CHECK_VERSION(major,minor,micro) \
(SLIRP_MAJOR_VERSION > (major) || \
(SLIRP_MAJOR_VERSION == (major) && SLIRP_MINOR_VERSION > (minor)) || \
(SLIRP_MAJOR_VERSION == (major) && SLIRP_MINOR_VERSION == (minor) && \
SLIRP_MICRO_VERSION >= (micro)))
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* LIBSLIRP_VERSION_H_ */
+426
View File
@@ -0,0 +1,426 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#ifndef LIBSLIRP_H
#define LIBSLIRP_H
#include <stdint.h>
#include <stdbool.h>
#include <sys/types.h>
#ifdef _WIN32
#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#include <in6addr.h>
#include <basetsd.h>
#include <errno.h>
typedef SSIZE_T slirp_ssize_t;
#ifdef LIBSLIRP_STATIC
# define SLIRP_EXPORT
#elif defined(BUILDING_LIBSLIRP)
# define SLIRP_EXPORT __declspec(dllexport)
#else
# define SLIRP_EXPORT __declspec(dllimport)
#endif
#else
#include <sys/types.h>
typedef ssize_t slirp_ssize_t;
#include <netinet/in.h>
#include <arpa/inet.h>
#define SLIRP_EXPORT
#endif
#include "libslirp-version.h"
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __GNUC__
#define SLIRP_DEPRECATED __attribute__((__deprecated__))
#else
#define SLIRP_DEPRECATED
#endif
/* Socket abstraction:*/
#if !defined(_WIN32)
/* Traditional Unix socket. */
typedef int slirp_os_socket;
#define SLIRP_INVALID_SOCKET (-1)
#define SLIRP_PRIfd "d"
#else
/* Windows: Win64 is a LLP64 platform, sizeof(int) < sizeof(long long) == sizeof(void *).
*
* Windows likes to pass HANDLE types around, which are pointers (aka unsigned long longs),
* which cannot be represented as ints. And, MS, in its infinite wisdom, decided to use
* a SOCKET handle instead of an int for socket library calls. */
typedef SOCKET slirp_os_socket;
#define SLIRP_INVALID_SOCKET INVALID_SOCKET
#if defined(_WIN64)
# define SLIRP_PRIfd "llx"
#else
# define SLIRP_PRIfd "x"
#endif
#endif
/* Opaque structure containing the slirp state */
typedef struct Slirp Slirp;
/* Flags passed to SlirpAddPollCb and to be returned by SlirpGetREventsCb. */
enum {
SLIRP_POLL_IN = 1 << 0,
SLIRP_POLL_OUT = 1 << 1,
SLIRP_POLL_PRI = 1 << 2,
SLIRP_POLL_ERR = 1 << 3,
SLIRP_POLL_HUP = 1 << 4,
};
/* Debugging flags. */
enum {
SLIRP_DBG_CALL = 1 << 0,
SLIRP_DBG_MISC = 1 << 1,
SLIRP_DBG_ERROR = 1 << 2,
SLIRP_DBG_TFTP = 1 << 3,
SLIRP_DBG_VERBOSE_CALL = 1 << 4,
};
/* Callback for application to get data from the guest */
typedef slirp_ssize_t (*SlirpReadCb)(void *buf, size_t len, void *opaque);
/* Callback for application to send data to the guest */
typedef slirp_ssize_t (*SlirpWriteCb)(const void *buf, size_t len, void *opaque);
/* Timer callback */
typedef void (*SlirpTimerCb)(void *opaque);
/* This is deprecated, use SlirpAddPollSocketCb instead */
typedef int (*SlirpAddPollCb)(int fd, int events, void *opaque);
/* Callback for libslirp to register polling callbacks */
typedef int (*SlirpAddPollSocketCb)(slirp_os_socket fd, int events, void *opaque);
/* Callback for libslirp to get polling result */
typedef int (*SlirpGetREventsCb)(int idx, void *opaque);
/* For now libslirp creates only a timer for the IPv6 RA */
typedef enum SlirpTimerId {
SLIRP_TIMER_RA,
SLIRP_TIMER_NUM,
} SlirpTimerId;
/*
* Callbacks from slirp, to be set by the application.
*
* The opaque parameter is set to the opaque pointer given in the slirp_new /
* slirp_init call.
*/
typedef struct SlirpCb {
/*
* Send an ethernet frame to the guest network. The opaque parameter is the
* one given to slirp_init(). If the guest is not ready to receive a frame,
* the function can just drop the data. TCP will then handle retransmissions
* at a lower pace.
* <0 reports an IO error.
*/
SlirpWriteCb send_packet;
/* Print a message for an error due to guest misbehavior. */
void (*guest_error)(const char *msg, void *opaque);
/* Return the virtual clock value in nanoseconds */
int64_t (*clock_get_ns)(void *opaque);
/* Create a new timer with the given callback and opaque data. Not
* needed if timer_new_opaque is provided. */
void *(*timer_new)(SlirpTimerCb cb, void *cb_opaque, void *opaque);
/* Remove and free a timer */
void (*timer_free)(void *timer, void *opaque);
/* Modify a timer to expire at @expire_time (ms) */
void (*timer_mod)(void *timer, int64_t expire_time, void *opaque);
/* Deprecated, use register_poll_socket instead */
void (*register_poll_fd)(int fd, void *opaque) SLIRP_DEPRECATED;
/* Deprecated, use unregister_poll_socket instead */
void (*unregister_poll_fd)(int fd, void *opaque) SLIRP_DEPRECATED;
/* Kick the io-thread, to signal that new events may be processed because some TCP buffer
* can now receive more data, i.e. slirp_socket_can_recv will return 1. */
void (*notify)(void *opaque);
/*
* Fields introduced in SlirpConfig version 4 begin
*/
/* Initialization has completed and a Slirp* has been created. */
void (*init_completed)(Slirp *slirp, void *opaque);
/* Create a new timer. When the timer fires, the application passes
* the SlirpTimerId and cb_opaque to slirp_handle_timer. */
void *(*timer_new_opaque)(SlirpTimerId id, void *cb_opaque, void *opaque);
/*
* Fields introduced in SlirpConfig version 6 begin
*/
/* Register a socket for future polling */
void (*register_poll_socket)(slirp_os_socket socket, void *opaque);
/* Unregister a socket */
void (*unregister_poll_socket)(slirp_os_socket socket, void *opaque);
} SlirpCb;
#define SLIRP_CONFIG_VERSION_MIN 1
#define SLIRP_CONFIG_VERSION_MAX 6
typedef struct SlirpConfig {
/* Version must be provided */
uint32_t version;
/*
* Fields introduced in SlirpConfig version 1 begin
*/
/* Whether to prevent the guest from accessing the Internet */
int restricted;
/* Whether IPv4 is enabled */
bool in_enabled;
/* Virtual network for the guest */
struct in_addr vnetwork;
/* Mask for the virtual network for the guest */
struct in_addr vnetmask;
/* Virtual address for the host exposed to the guest */
struct in_addr vhost;
/* Whether IPv6 is enabled */
bool in6_enabled;
/* Virtual IPv6 network for the guest */
struct in6_addr vprefix_addr6;
/* Len of the virtual IPv6 network for the guest */
uint8_t vprefix_len;
/* Virtual address for the host exposed to the guest */
struct in6_addr vhost6;
/* Hostname exposed to the guest in DHCP hostname option */
const char *vhostname;
/* Hostname exposed to the guest in the DHCP TFTP server name option */
const char *tftp_server_name;
/* Path of the files served by TFTP */
const char *tftp_path;
/* Boot file name exposed to the guest via DHCP */
const char *bootfile;
/* Start of the DHCP range */
struct in_addr vdhcp_start;
/* Virtual address for the DNS server exposed to the guest */
struct in_addr vnameserver;
/* Virtual IPv6 address for the DNS server exposed to the guest */
struct in6_addr vnameserver6;
/* DNS search names exposed to the guest via DHCP */
const char **vdnssearch;
/* Domain name exposed to the guest via DHCP */
const char *vdomainname;
/* MTU when sending packets to the guest */
/* Default: IF_MTU_DEFAULT */
size_t if_mtu;
/* MRU when receiving packets from the guest */
/* Default: IF_MRU_DEFAULT */
size_t if_mru;
/* Prohibit connecting to 127.0.0.1:* */
bool disable_host_loopback;
/*
* Enable emulation code (*warning*: this code isn't safe, it is not
* recommended to enable it)
*/
bool enable_emu;
/*
* Fields introduced in SlirpConfig version 2 begin
*/
/* Address to be used when sending data to the Internet */
struct sockaddr_in *outbound_addr;
/* IPv6 Address to be used when sending data to the Internet */
struct sockaddr_in6 *outbound_addr6;
/*
* Fields introduced in SlirpConfig version 3 begin
*/
/* slirp will not redirect/serve any DNS packet */
bool disable_dns;
/*
* Fields introduced in SlirpConfig version 4 begin
*/
/* slirp will not reply to any DHCP requests */
bool disable_dhcp;
/*
* Fields introduced in SlirpConfig version 5 begin
*/
/* Manufacturer ID (IANA Private Enterprise number) */
uint32_t mfr_id;
/*
* MAC address allocated for an out-of-band management controller, to be
* retrieved through NC-SI.
*/
uint8_t oob_eth_addr[6];
} SlirpConfig;
/* Create a new instance of a slirp stack */
SLIRP_EXPORT
Slirp *slirp_new(const SlirpConfig *cfg, const SlirpCb *callbacks,
void *opaque);
/* slirp_init is deprecated in favor of slirp_new */
SLIRP_EXPORT
Slirp *slirp_init(int restricted, bool in_enabled, struct in_addr vnetwork,
struct in_addr vnetmask, struct in_addr vhost,
bool in6_enabled, struct in6_addr vprefix_addr6,
uint8_t vprefix_len, struct in6_addr vhost6,
const char *vhostname, const char *tftp_server_name,
const char *tftp_path, const char *bootfile,
struct in_addr vdhcp_start, struct in_addr vnameserver,
struct in6_addr vnameserver6, const char **vdnssearch,
const char *vdomainname, const SlirpCb *callbacks,
void *opaque);
/* Shut down an instance of a slirp stack */
SLIRP_EXPORT
void slirp_cleanup(Slirp *slirp);
/* This is deprecated, use slirp_pollfds_fill_socket instead. */
SLIRP_EXPORT
void slirp_pollfds_fill(Slirp *slirp, uint32_t *timeout,
SlirpAddPollCb add_poll, void *opaque) SLIRP_DEPRECATED;
/* This is called by the application when it is about to sleep through poll().
* *timeout is set to the amount of virtual time (in ms) that the application intends to
* wait (UINT32_MAX if infinite). slirp_pollfds_fill updates it according to
* e.g. TCP timers, so the application knows it should sleep a smaller amount of
* time. slirp_pollfds_fill calls add_poll for each file descriptor
* that should be monitored along the sleep. The opaque pointer is passed as
* such to add_poll, and add_poll returns an index. */
SLIRP_EXPORT
void slirp_pollfds_fill_socket(Slirp *slirp, uint32_t *timeout,
SlirpAddPollSocketCb add_poll, void *opaque);
/* This is called by the application after sleeping, to report which file
* descriptors are available. slirp_pollfds_poll calls get_revents on each file
* descriptor, giving it the index that add_poll returned during the
* slirp_pollfds_fill call, to know whether the descriptor is available for
* read/write/etc. (SLIRP_POLL_*)
* select_error should be passed 1 if poll() returned an error. */
SLIRP_EXPORT
void slirp_pollfds_poll(Slirp *slirp, int select_error,
SlirpGetREventsCb get_revents, void *opaque);
/* This is called by the application when the guest emits a packet on the
* guest network, to be interpreted by slirp. */
SLIRP_EXPORT
void slirp_input(Slirp *slirp, const uint8_t *pkt, int pkt_len);
/* This is called by the application when a timer expires, if it provides
* the timer_new_opaque callback. It is not needed if the application only
* uses timer_new. */
SLIRP_EXPORT
void slirp_handle_timer(Slirp *slirp, SlirpTimerId id, void *cb_opaque);
/* These set up / remove port forwarding between a host port in the real world
* and the guest network.
* Note: guest_addr must be in network order, while guest_port must be in host
* order.
*/
SLIRP_EXPORT
int slirp_add_hostfwd(Slirp *slirp, int is_udp, struct in_addr host_addr,
int host_port, struct in_addr guest_addr, int guest_port);
SLIRP_EXPORT
int slirp_remove_hostfwd(Slirp *slirp, int is_udp, struct in_addr host_addr,
int host_port);
#define SLIRP_HOSTFWD_UDP 1
#define SLIRP_HOSTFWD_V6ONLY 2
SLIRP_EXPORT
int slirp_add_hostxfwd(Slirp *slirp,
const struct sockaddr *haddr, socklen_t haddrlen,
const struct sockaddr *gaddr, socklen_t gaddrlen,
int flags);
SLIRP_EXPORT
int slirp_remove_hostxfwd(Slirp *slirp,
const struct sockaddr *haddr, socklen_t haddrlen,
int flags);
/* Set up port forwarding between a port in the guest network and a
* command running on the host */
SLIRP_EXPORT
int slirp_add_exec(Slirp *slirp, const char *cmdline,
struct in_addr *guest_addr, int guest_port);
/* Set up port forwarding between a port in the guest network and a
* Unix port on the host */
SLIRP_EXPORT
int slirp_add_unix(Slirp *slirp, const char *unixsock,
struct in_addr *guest_addr, int guest_port);
/* Set up port forwarding between a port in the guest network and a
* callback that will receive the data coming from the port */
SLIRP_EXPORT
int slirp_add_guestfwd(Slirp *slirp, SlirpWriteCb write_cb, void *opaque,
struct in_addr *guest_addr, int guest_port);
/* TODO: rather identify a guestfwd through an opaque pointer instead of through
* the guest_addr */
/* This is called by the application for a guestfwd, to determine how much data
* can be received by the forwarded port through a call to slirp_socket_recv. */
SLIRP_EXPORT
size_t slirp_socket_can_recv(Slirp *slirp, struct in_addr guest_addr,
int guest_port);
/* This is called by the application for a guestfwd, to provide the data to be
* sent on the forwarded port */
SLIRP_EXPORT
void slirp_socket_recv(Slirp *slirp, struct in_addr guest_addr, int guest_port,
const uint8_t *buf, int size);
/* Remove entries added by slirp_add_exec, slirp_add_unix or slirp_add_guestfwd */
SLIRP_EXPORT
int slirp_remove_guestfwd(Slirp *slirp, struct in_addr guest_addr,
int guest_port);
/* Return a human-readable state of the slirp stack */
SLIRP_EXPORT
char *slirp_connection_info(Slirp *slirp);
/* Return a human-readable state of the NDP/ARP tables */
SLIRP_EXPORT
char *slirp_neighbor_info(Slirp *slirp);
/* Save the slirp state through the write_cb. The opaque pointer is passed as
* such to the write_cb. */
SLIRP_EXPORT
int slirp_state_save(Slirp *s, SlirpWriteCb write_cb, void *opaque);
/* Returns the version of the slirp state, to be saved along the state */
SLIRP_EXPORT
int slirp_state_version(void);
/* Load the slirp state through the read_cb. The opaque pointer is passed as
* such to the read_cb. The version should be given as it was obtained from
* slirp_state_version when slirp_state_save was called. */
SLIRP_EXPORT
int slirp_state_load(Slirp *s, int version_id, SlirpReadCb read_cb,
void *opaque);
/* Return the version of the slirp implementation */
SLIRP_EXPORT
const char *slirp_version_string(void);
/* Debugging support: There are two methods for enabling debugging
* in libslirp: the SLIRP_DEBUG environment variable and the
* slirp_(set|reset)_debug() functions.
*
* SLIRP_DEBUG is a list of debug options separated by colons, spaces
* or commas. Valid debug options are 'call', 'misc', 'error', 'tftp'
* and 'verbose_call'.
*/
/* Set debugging flags independently of the SLIRP_DEBUG environment
* variable. */
SLIRP_EXPORT
void slirp_set_debug(unsigned int flags);
/* Reset debugging flags. */
SLIRP_EXPORT
void slirp_reset_debug(unsigned int flags);
#if defined(_WIN32)
/* Windows utility functions: */
/* inet_aton() replacement that uses inet_pton(). Eliminates the dreaded
* winsock2 deprecation messages. */
SLIRP_EXPORT
int slirp_inet_aton(const char *cp, struct in_addr *ia);
#endif
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* LIBSLIRP_H */
+42
View File
@@ -0,0 +1,42 @@
[
{
"name": "libslirp",
"url": "https://mirror.msys2.org/mingw/ucrt64/mingw-w64-ucrt-x86_64-libslirp-4.9.3-1-any.pkg.tar.zst",
"sha256": "c982be43767a950f220b498f0f212eb2e2c945d93c968bd0e4f66b1b5da94f6c"
},
{
"name": "glib2",
"url": "https://mirror.msys2.org/mingw/ucrt64/mingw-w64-ucrt-x86_64-glib2-2.88.3-1-any.pkg.tar.zst",
"sha256": "0733a674ecf5282088158b8e458cb2ad3359bf42fb7dbe9561b4bcfe0267f5bf"
},
{
"name": "gettext-runtime",
"url": "https://mirror.msys2.org/mingw/ucrt64/mingw-w64-ucrt-x86_64-gettext-runtime-1.0-1-any.pkg.tar.zst",
"sha256": "ba693dda4ac375af76ce481ff3a6e7481286546cc7dc6d56c7021dae34084157"
},
{
"name": "libffi",
"url": "https://mirror.msys2.org/mingw/ucrt64/mingw-w64-ucrt-x86_64-libffi-3.8.0-1-any.pkg.tar.zst",
"sha256": "99ad12f4ecfa00a889ef9c5c0188592368a5bac109d0c508ce4e04f29ea95a76"
},
{
"name": "pcre2",
"url": "https://mirror.msys2.org/mingw/ucrt64/mingw-w64-ucrt-x86_64-pcre2-10.48-3-any.pkg.tar.zst",
"sha256": "f1aae310be1c65ec0e2265d283ba01fcf322208bc19c96cfa52801fcc1c11746"
},
{
"name": "libiconv",
"url": "https://mirror.msys2.org/mingw/ucrt64/mingw-w64-ucrt-x86_64-libiconv-1.19-1-any.pkg.tar.zst",
"sha256": "9a500f38c2b91808741c62fae746b3e9110b33a1ecf5c30fa0c66dbedddf7e16"
},
{
"name": "libwinpthread",
"url": "https://mirror.msys2.org/mingw/ucrt64/mingw-w64-ucrt-x86_64-libwinpthread-14.0.0.r353.g6df76fa52-2-any.pkg.tar.zst",
"sha256": "ec1a46e63d424a90992a14c0996c0b5bb17370af6044380b590d05f725eeb43f"
},
{
"name": "gcc-libs",
"url": "https://mirror.msys2.org/mingw/ucrt64/mingw-w64-ucrt-x86_64-gcc-libs-16.2.0-3-any.pkg.tar.zst",
"sha256": "5763fabf86fa13a4449ee765006d3446384ed66af7bf827459710eb777e0b11c"
}
]
+10
View File
@@ -7,6 +7,8 @@ param(
[switch]$SafeTextureCache = $true,
[switch]$Ethernet,
[switch]$Wait
)
@@ -18,10 +20,14 @@ if (-not (Test-Path -LiteralPath $dolphinPath)) {
throw "DolphinNoGUI.exe is missing: $dolphinPath"
}
# NoGUI's file log listener expects the parent directory to exist on first launch.
New-Item -ItemType Directory -Force -Path (Join-Path $userPath 'Logs') | Out-Null
$dolphinArguments = @(
'-u', $userPath,
'-n', '0000000100000002',
'-C', "Dolphin.Core.WiiStarletJIT=$(-not $DisableJIT)",
'-C', "Dolphin.Core.WiiLLEEthernet=$($Ethernet.IsPresent)",
# Keep the next BootMii run diagnostic rather than observational. BootMii reports its loader
# stages and panic codes through GPIO bits 16-23; the Starlet bus logs those writes under IOS.
'-C', 'Logger.Options.WriteToFile=True',
@@ -37,6 +43,10 @@ if ($SafeTextureCache) {
$dolphinArguments += @('-C', 'Graphics.Settings.SafeTextureCacheColorSamples=0')
}
if ($Ethernet) {
$dolphinArguments += @('-C', 'Logger.Logs.IOS_USB=True', '-C', 'Logger.Logs.IOS_NET=True')
}
$dolphin = Start-Process -FilePath $dolphinPath -ArgumentList $dolphinArguments `
-WorkingDirectory $repoRoot -PassThru
+39
View File
@@ -0,0 +1,39 @@
param([string]$BuildDirectory = '.starlet_msvc2')
$ErrorActionPreference = 'Stop'
$repoRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$cacheRoot = Join-Path $repoRoot '.starlet_network'
$runtimeRoot = Join-Path $repoRoot "$BuildDirectory\Binaries\Network"
$packages = Get-Content -Raw (Join-Path $repoRoot 'Externals\libslirp\runtime-msys2.json') | ConvertFrom-Json
New-Item -ItemType Directory -Force -Path $cacheRoot, $runtimeRoot | Out-Null
foreach ($package in $packages) {
$archive = Join-Path $cacheRoot ([Uri]$package.url).Segments[-1]
if (-not (Test-Path -LiteralPath $archive)) {
Write-Host "Downloading $($package.name)..."
Invoke-WebRequest -Uri $package.url -OutFile $archive
}
if ((Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash -ne $package.sha256) {
throw "SHA256 mismatch: $archive (preserved for inspection)"
}
# Extract only DLLs and licenses from verified packages; no installer or global PATH changes.
$entries = @(tar -tf $archive)
if ($LASTEXITCODE -ne 0) { throw "Cannot list $archive" }
$selected = @($entries | Where-Object {
$_ -match '^ucrt64/bin/[^/]+\.dll$' -or $_ -match '^ucrt64/share/licenses/[^/]+/[^/]+$'
})
if ($selected.Count -eq 0) { throw "No runtime files in $archive" }
$extracted = Join-Path $cacheRoot $package.name
New-Item -ItemType Directory -Force -Path $extracted | Out-Null
tar -xf $archive -C $extracted @selected
if ($LASTEXITCODE -ne 0) { throw "Cannot extract $archive" }
Get-ChildItem -LiteralPath (Join-Path $extracted 'ucrt64\bin') -Filter '*.dll' -File |
Copy-Item -Destination $runtimeRoot
$licenses = Join-Path $extracted 'ucrt64\share\licenses'
if (Test-Path -LiteralPath $licenses) {
$licenseDestination = Join-Path $runtimeRoot "licenses\$($package.name)"
New-Item -ItemType Directory -Force -Path $licenseDestination | Out-Null
Get-ChildItem -LiteralPath $licenses -File -Recurse | Copy-Item -Destination $licenseDestination
}
}
Write-Host "Local libslirp runtime ready: $runtimeRoot"
+6
View File
@@ -404,8 +404,12 @@ add_library(core
IOS/MIOS.h
IOS/Starlet/ARMCore.cpp
IOS/Starlet/ARMCore.h
IOS/Starlet/AX88772.cpp
IOS/Starlet/AX88772.h
IOS/Starlet/NANDJournal.cpp
IOS/Starlet/NANDJournal.h
IOS/Starlet/SlirpNetwork.cpp
IOS/Starlet/SlirpNetwork.h
IOS/Starlet/Starlet.cpp
IOS/Starlet/Starlet.h
IOS/Starlet/StarletMemory.cpp
@@ -859,6 +863,8 @@ if(MSVC)
target_link_libraries(core PRIVATE use_pch)
endif()
target_include_directories(core PRIVATE ${PROJECT_SOURCE_DIR}/Externals/libslirp/include)
if(USE_RETRO_ACHIEVEMENTS)
target_link_libraries(core PUBLIC rcheevos)
target_compile_definitions(core PUBLIC -DUSE_RETRO_ACHIEVEMENTS)
+1
View File
@@ -258,6 +258,7 @@ const Info<std::string> MAIN_GPU_DETERMINISM_MODE{{System::Main, "Core", "GPUDet
const Info<s32> MAIN_OVERRIDE_BOOT_IOS{{System::Main, "Core", "OverrideBootIOS"}, -1};
const Info<bool> MAIN_WII_IOS_LLE{{System::Main, "Core", "WiiIOSLLE"}, false};
const Info<bool> MAIN_WII_STARLET_JIT{{System::Main, "Core", "WiiStarletJIT"}, true};
const Info<bool> MAIN_WII_LLE_ETHERNET{{System::Main, "Core", "WiiLLEEthernet"}, false};
GPUDeterminismMode GetGPUDeterminismMode()
{
+1
View File
@@ -168,6 +168,7 @@ extern const Info<bool> MAIN_REAL_WII_REMOTE_REPEAT_REPORTS;
extern const Info<s32> MAIN_OVERRIDE_BOOT_IOS;
extern const Info<bool> MAIN_WII_IOS_LLE;
extern const Info<bool> MAIN_WII_STARLET_JIT;
extern const Info<bool> MAIN_WII_LLE_ETHERNET;
extern const Info<std::string> MAIN_WII_NUS_SHOP_URL;
extern const Info<bool> MAIN_WII_WIILINK_ENABLE;
+4 -2
View File
@@ -33,12 +33,14 @@ enum StarletInterruptCause
INT_CAUSE_OHCI1 = 0x40,
INT_CAUSE_SD = 0x80,
INT_CAUSE_WIFI = 0x100,
INT_CAUSE_DI = 0x200,
INT_CAUSE_GPIO_BROADWAY = 0x400,
INT_CAUSE_GPIO_STARLET = 0x800,
INT_CAUSE_RST_BUTTON = 0x40000,
// Hollywood IRQ17/18, distinct from the Broadway PI interrupt numbers.
// https://wiibrew.org/wiki/Hollywood/IRQs
INT_CAUSE_RST_BUTTON = 0x20000,
INT_CAUSE_DI = 0x40000,
INT_CAUSE_IPC_BROADWAY = 0x40000000,
INT_CAUSE_IPC_STARLET = 0x80000000
+64 -19
View File
@@ -76,10 +76,10 @@ ARMJitX64::ARMJitX64(ARMCore& core) : m_core(core)
m_executed_instructions_offset =
static_cast<s32>(reinterpret_cast<const u8*>(&m_core.m_executed_instructions) - base);
m_control_offset = static_cast<s32>(reinterpret_cast<const u8*>(&m_core.m_cp15.control) - base);
m_translation_table_base_offset = static_cast<s32>(
reinterpret_cast<const u8*>(&m_core.m_cp15.translation_table_base) - base);
m_domain_access_control_offset = static_cast<s32>(
reinterpret_cast<const u8*>(&m_core.m_cp15.domain_access_control) - base);
m_translation_table_base_offset =
static_cast<s32>(reinterpret_cast<const u8*>(&m_core.m_cp15.translation_table_base) - base);
m_domain_access_control_offset =
static_cast<s32>(reinterpret_cast<const u8*>(&m_core.m_cp15.domain_access_control) - base);
m_process_id_offset =
static_cast<s32>(reinterpret_cast<const u8*>(&m_core.m_cp15.process_id) - base);
m_tlb_generation_offset =
@@ -240,15 +240,13 @@ void ARMJitX64::GenerateDispatcher()
SetJumpTarget(no_mmu_way1_key_miss);
CMP(32, MDisp(R11, static_cast<s32>(2 * sizeof(FastEntry) + offsetof(FastEntry, key))), R(EAX));
FixupBranch no_mmu_way2_key_miss = J_CC(CC_NE, Jump::Near);
CMP(32, MDisp(R11,
static_cast<s32>(2 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))),
CMP(32, MDisp(R11, static_cast<s32>(2 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))),
R(R8));
FixupBranch no_mmu_hit_way2 = J_CC(CC_E, Jump::Near);
SetJumpTarget(no_mmu_way2_key_miss);
CMP(32, MDisp(R11, static_cast<s32>(3 * sizeof(FastEntry) + offsetof(FastEntry, key))), R(EAX));
FixupBranch no_mmu_cache_miss_key = J_CC(CC_NE, Jump::Near);
CMP(32, MDisp(R11,
static_cast<s32>(3 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))),
CMP(32, MDisp(R11, static_cast<s32>(3 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))),
R(R8));
FixupBranch no_mmu_cache_miss_page = J_CC(CC_NE, Jump::Near);
ADD(64, R(R11), Imm8(static_cast<u8>(3 * sizeof(FastEntry))));
@@ -331,15 +329,13 @@ void ARMJitX64::GenerateDispatcher()
SetJumpTarget(mmu_way1_key_miss);
CMP(32, MDisp(R11, static_cast<s32>(2 * sizeof(FastEntry) + offsetof(FastEntry, key))), R(EAX));
FixupBranch mmu_way2_key_miss = J_CC(CC_NE, Jump::Near);
CMP(32, MDisp(R11,
static_cast<s32>(2 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))),
CMP(32, MDisp(R11, static_cast<s32>(2 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))),
R(R8));
FixupBranch mmu_hit_way2 = J_CC(CC_E, Jump::Near);
SetJumpTarget(mmu_way2_key_miss);
CMP(32, MDisp(R11, static_cast<s32>(3 * sizeof(FastEntry) + offsetof(FastEntry, key))), R(EAX));
FixupBranch mmu_cache_miss_key = J_CC(CC_NE, Jump::Near);
CMP(32, MDisp(R11,
static_cast<s32>(3 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))),
CMP(32, MDisp(R11, static_cast<s32>(3 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))),
R(R8));
FixupBranch mmu_cache_miss_page = J_CC(CC_NE, Jump::Near);
ADD(64, R(R11), Imm8(static_cast<u8>(3 * sizeof(FastEntry))));
@@ -1340,8 +1336,8 @@ bool ARMJitX64::CanEmitARMMemory(u32 instruction) const
const bool direct_pc_load = load && rd == 15 && (instruction >> 28) == 0xe && preindex &&
!writeback && (instruction & (1U << 22)) == 0;
return (rd != 15 || direct_pc_load) && !(rn == 15 && (!preindex || writeback)) &&
!(load && writeback && rn == rd) &&
!(register_offset && (instruction & (1U << 4)) != 0) && !(register_offset && rm == 15);
!(load && writeback && rn == rd) && !(register_offset && (instruction & (1U << 4)) != 0) &&
!(register_offset && rm == 15);
}
bool ARMJitX64::EmitARMHalfwordMemory(u32 instruction, u32 address)
@@ -2177,7 +2173,8 @@ bool ARMJitX64::EmitThumbMemory(u16 instruction, u32 address)
}
const FixupBranch direct_done = J();
EmitThumbMemorySlowPath(slow_paths, instruction, address, direct_done);
EmitThumbMemorySlowPath(slow_paths, instruction, address, direct_done, access_size, rd, load,
sign_extend);
return true;
}
@@ -2388,12 +2385,49 @@ void ARMJitX64::EmitFastmemAddress(std::vector<FixupBranch>* slow_paths, u32 acc
}
void ARMJitX64::EmitThumbMemorySlowPath(const std::vector<FixupBranch>& slow_paths, u16 instruction,
u32 address, FixupBranch direct_done)
u32 address, FixupBranch direct_done, u32 access_size,
u32 rd, bool load, bool sign_extend)
{
for (const FixupBranch& slow_path : slow_paths)
SetJumpTarget(slow_path);
EmitFallbackThumb(instruction, address);
EmitBlockExit(m_compile_instruction_count, m_compile_native_instruction_count);
// IOS Thumb code repeatedly accesses SRAM and MMIO. These instructions are already decoded:
// perform the exact bus transaction without re-entering ExecuteThumb and the block dispatcher.
// Do not broaden SRAM fastmem: boot0 protection, split-window holes, and device side effects
// remain the bus's responsibility. Keep unaligned accesses on the interpreter path, including
// register-offset word rotation and halfwords spanning two virtual translation granules.
FixupBranch unaligned;
if (access_size > 1)
{
TEST(32, R(R9), Imm32(access_size - 1));
unaligned = J_CC(CC_NZ, Jump::Near);
}
MOV(32, R(ABI_PARAM2), R(R9));
if (load)
{
MOV(32, R(ABI_PARAM4), Imm32(0));
MOV(32, R(ABI_PARAM3), Imm32(access_size));
MOV(64, R(ABI_PARAM1), ImmPtr(this));
ABI_CallFunction(ReadMemorySlow);
if (sign_extend)
MOVSX(32, access_size * 8, EAX, R(EAX));
MOV(32, MStoredRegister(rd), R(EAX));
}
else
{
MOV(32, R(ABI_PARAM4), MStoredRegister(rd));
MOV(32, R(ABI_PARAM3), Imm32(access_size));
MOV(64, R(ABI_PARAM1), ImmPtr(this));
ABI_CallFunction(WriteMemorySlow);
}
if (access_size > 1)
{
const FixupBranch bus_done = J(Jump::Near);
SetJumpTarget(unaligned);
EmitFallbackThumb(instruction, address);
EmitBlockExit(m_compile_instruction_count, m_compile_native_instruction_count);
SetJumpTarget(bus_done);
}
SetJumpTarget(direct_done);
LoadRegisterCache();
}
@@ -2926,7 +2960,7 @@ void ARMJitX64::ExceptionReturn(ARMJitX64* jit, u32 target)
}
u32 ARMJitX64::ReadMemorySlow(ARMJitX64* jit, u32 physical_address, u32 access_size,
u32 byte_offset)
u32 byte_offset)
{
++jit->m_slow_read_count;
if ((jit->m_slow_read_count & 0xff) == 0)
@@ -2952,6 +2986,11 @@ u32 ARMJitX64::ReadMemorySlow(ARMJitX64* jit, u32 physical_address, u32 access_s
ARMCore& core = jit->m_core;
if (access_size == 1)
return core.m_bus.Read8(physical_address);
if (access_size == 2)
{
const u16 value = core.m_bus.Read16(physical_address);
return core.m_big_endian ? value : std::byteswap(value);
}
u32 value = core.m_bus.Read32(physical_address & ~3U);
if (!core.m_big_endian)
@@ -2988,6 +3027,12 @@ void ARMJitX64::WriteMemorySlow(ARMJitX64* jit, u32 physical_address, u32 access
core.m_bus.Write8(physical_address, static_cast<u8>(value));
return;
}
if (access_size == 2)
{
const u16 halfword = static_cast<u16>(value);
core.m_bus.Write16(physical_address, core.m_big_endian ? halfword : std::byteswap(halfword));
return;
}
if (!core.m_big_endian)
value = std::byteswap(value);
+6 -8
View File
@@ -69,10 +69,7 @@ public:
{
return m_dispatch_key_miss_with_empty_slot_count;
}
u64 GetDispatchKeyMissWithFullSetCount() const
{
return m_dispatch_key_miss_with_full_set_count;
}
u64 GetDispatchKeyMissWithFullSetCount() const { return m_dispatch_key_miss_with_full_set_count; }
u64 GetDispatchKeyMissWithStaleTranslationCount() const
{
return m_dispatch_key_miss_with_stale_translation_count;
@@ -171,9 +168,9 @@ private:
static size_t GetFastEntrySetIndex(u32 key);
static const u8* Dispatch(ARMJitX64* jit, DispatchReason reason, u32 generated_key,
u32 generated_set_offset);
Block* GetOrCompileBlock(u32 address, u32* physical_address_out,
const u8** first_descriptor_out, u32* first_descriptor_value_out,
const u8** second_descriptor_out, u32* second_descriptor_value_out);
Block* GetOrCompileBlock(u32 address, u32* physical_address_out, const u8** first_descriptor_out,
u32* first_descriptor_value_out, const u8** second_descriptor_out,
u32* second_descriptor_value_out);
Block CompileBlock(u32 address, bool thumb);
bool EmitDirectARM(u32 instruction, u32 address, bool* terminal, bool* dispatcher_exit);
bool EmitARMMultiplyLong(u32 instruction);
@@ -193,7 +190,8 @@ private:
SRAMFastmemAccess sram_access = SRAMFastmemAccess::None,
bool arm_unaligned_word = false);
void EmitThumbMemorySlowPath(const std::vector<Gen::FixupBranch>& slow_paths, u16 instruction,
u32 address, Gen::FixupBranch direct_done);
u32 address, Gen::FixupBranch direct_done, u32 access_size, u32 rd,
bool load, bool sign_extend);
void EmitARMMemorySlowPath(const std::vector<Gen::FixupBranch>& slow_paths, u32 instruction,
u32 address, Gen::FixupBranch direct_done);
void EmitThumbAddSub(u16 instruction);
+579
View File
@@ -0,0 +1,579 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/IOS/Starlet/AX88772.h"
#include <algorithm>
#include <string_view>
#include <utility>
#include <fmt/ranges.h>
#include "Common/ChunkFile.h"
#include "Common/Logging/Log.h"
namespace IOS::LLE
{
namespace
{
u16 LE16(std::span<const u8> bytes, size_t offset = 0)
{
return u16(bytes[offset]) | (u16(bytes[offset + 1]) << 8);
}
std::vector<u8> Word(u16 value)
{
return {static_cast<u8>(value), static_cast<u8>(value >> 8)};
}
constexpr size_t MAX_FRAME = 1518;
constexpr size_t MAX_QUEUE = 128;
} // namespace
AX88772::AX88772(Transmit transmit) : m_transmit(std::move(transmit))
{
m_eeprom.fill(0xffff);
for (size_t i = 0; i < 3; ++i)
m_eeprom[4 + i] = LE16(m_mac, i * 2);
// Datasheet section 4.1.6: primary embedded PHY 0x10; no secondary PHY (0xe0).
// IOS also reads this EEPROM word directly, not only USB command 0x19.
m_eeprom[0x11] = 0xe010;
m_eeprom[0x0f] = 0x0105; // Interrupt reports carry BMSR and link partner abilities.
m_eeprom[0x10] = MAX_FRAME;
Reset();
}
void AX88772::Reset()
{
m_address = m_configuration = 0;
m_control_valid = m_control_stalled = false;
m_control.clear();
m_out.clear();
m_tx.clear();
m_rx.clear();
m_rx_offset = m_control_offset = 0;
m_phy.fill(0);
m_phy[0] = 0x3100; // Autonegotiation, 100 Mbps, full duplex.
m_phy[2] = 0x003b;
m_phy[3] = 0x1861; // AX88772A internal PHY, as identified by Linux asix.
m_phy[4] = 0x01e1;
m_phy[5] = 0x45e1;
m_phy[6] = 1;
m_rx_control = m_medium = m_gpio = 0;
m_monitor = 0;
m_ipg = {0x15, 0x0c, 0x12};
m_multicast.fill(0);
m_phy_select = 1;
m_software_reset = 0x28;
m_software_mii = m_eeprom_writable = false;
m_link_event = true;
m_status_frames_remaining = 0;
m_control_log_count = 0;
m_interrupt_reports = 0;
}
bool AX88772::LinkUp() const
{
return m_connected && (m_phy[0] & 0x0800) == 0 && (m_software_reset & 0x40) == 0;
}
void AX88772::DoState(PointerWrap& p)
{
p.Do(m_mac);
p.Do(m_eeprom);
p.Do(m_phy);
p.Do(m_multicast);
p.Do(m_ipg);
p.Do(m_setup);
p.Do(m_control);
p.Do(m_out);
p.Do(m_tx);
p.Do(m_rx);
p.Do(m_control_offset);
p.Do(m_rx_offset);
p.Do(m_rx_control);
p.Do(m_medium);
p.Do(m_gpio);
p.Do(m_monitor);
p.Do(m_phy_select);
p.Do(m_software_reset);
p.Do(m_address);
p.Do(m_configuration);
p.Do(m_control_valid);
p.Do(m_control_stalled);
p.Do(m_software_mii);
p.Do(m_eeprom_writable);
p.Do(m_connected);
p.Do(m_link_event);
p.Do(m_status_frames_remaining);
if (p.IsReadMode() &&
(m_control_offset > m_control.size() ||
(!m_rx.empty() && m_rx_offset > m_rx.front().size()) || m_tx.size() > 65536 ||
m_rx.size() > MAX_QUEUE || m_status_frames_remaining > INTERRUPT_INTERVAL_MS))
p.SetVerifyMode();
}
void AX88772::SetLink(bool connected)
{
if (m_connected != connected)
m_link_event = true;
m_connected = connected;
}
void AX88772::AdvanceUSBFrame()
{
// The status endpoint supplies periodic PHY snapshots, not just link-change
// events (AX88772 datasheet 6.2.1.7). IOS reads it synchronously to check an
// unchanged link too. Keep a single pending snapshot: USB scheduling delays
// must not build up a queue of stale reports or produce a catch-up burst.
if (m_configuration != 0 && m_status_frames_remaining != 0 && --m_status_frames_remaining == 0)
m_link_event = true;
}
u16 AX88772::ReadPHY(u16 reg) const
{
if (reg >= m_phy.size())
return 0xffff;
if (reg == 1)
return 0x7809 | (LinkUp() ? 0x0024 : 0); // Capabilities, link and autoneg complete.
return m_phy[reg];
}
std::vector<u8> AX88772::Descriptor(u16 value) const
{
switch (value >> 8)
{
case 1:
return {18, 1, 0, 2, 0xff, 0xff, 0, 64, 0x95, 0x0b, 0x20, 0x77, 1, 0, 1, 2, 3, 1};
case 2:
// Interrupt IN 1, bulk IN 2, bulk OUT 3. Full-speed bulk packets are 64 bytes.
return {9, 2, 39, 0, 1,
1, 0, 0x80, 125, 9,
4, 0, 0, 3, 0xff,
0xff, 0, 0, 7, 5,
0x81, 3, 8, 0, INTERRUPT_INTERVAL_MS,
7, 5, 0x82, 2, 64,
0, 0, 7, 5, 3,
2, 64, 0, 0};
case 3:
{
if ((value & 0xff) == 0)
return {4, 3, 9, 4};
constexpr std::array<std::string_view, 3> strings{"ASIX", "AX88772A USB Ethernet",
"DolphinLLE0001"};
const u8 index = value & 0xff;
if (index > strings.size())
return {};
const auto str = strings[index - 1];
std::vector<u8> descriptor{static_cast<u8>(2 + str.size() * 2), 3};
for (char c : str)
{
descriptor.push_back(static_cast<u8>(c));
descriptor.push_back(0);
}
return descriptor;
}
default:
return {};
}
}
bool AX88772::PrepareControl()
{
const u8 type = m_setup[0], request = m_setup[1];
const u16 value = LE16(m_setup, 2), index = LE16(m_setup, 4);
const u16 length = LE16(m_setup, 6);
m_control.clear();
m_out.clear();
m_control_offset = 0;
if ((type & 0x60) == 0)
{
if ((type & 0x80) != 0)
{
switch (request)
{
case 0:
m_control = {0, 0};
break;
case 6:
m_control = Descriptor(value);
if (m_control.empty())
return false;
break;
case 8:
m_control = {m_configuration};
break;
case 10:
m_control = {0};
break;
default:
return false;
}
}
else if (length != 0 ||
(request != 1 && request != 3 && request != 5 && request != 9 && request != 11))
return false;
}
else if ((type & 0x60) == 0x40)
{
// Register protocol documented by the Linux drivers/net/usb/asix sources.
if ((type & 0x80) != 0)
{
switch (request)
{
case 0x07:
m_control = Word((value & 0x1f) == 0x10 ? ReadPHY(index & 0x1f) : 0xffff);
break;
case 0x09:
m_control = {static_cast<u8>(m_software_mii ? 1 : 0)};
break;
case 0x0b:
if (value >= m_eeprom.size())
return false;
m_control = Word(m_eeprom[value]);
INFO_LOG_FMT(IOS_USB, "AX88772 EEPROM read word {:#04x} = {:#06x}", value, m_eeprom[value]);
break;
case 0x0f:
m_control = Word(m_rx_control);
break;
case 0x11:
m_control.assign(m_ipg.begin(), m_ipg.end());
break;
case 0x13:
m_control.assign(m_mac.begin(), m_mac.end());
break;
case 0x15:
m_control.assign(m_multicast.begin(), m_multicast.end());
break;
case 0x19:
m_control = {static_cast<u8>(m_eeprom[0x11] >> 8), static_cast<u8>(m_eeprom[0x11])};
break;
case 0x1a:
m_control = Word(m_medium);
break;
case 0x1c:
m_control = {m_monitor};
break;
case 0x1e:
m_control = Word(m_gpio);
break;
case 0x21:
m_control = {0x10};
break; // AX88772A chip code, internal PHY.
default:
return false;
}
}
else
{
switch (request)
{
case 0x06:
case 0x0a:
case 0x0d:
case 0x0e:
case 0x10:
case 0x12:
case 0x1b:
case 0x1d:
case 0x1f:
case 0x20:
case 0x22:
if (length != 0)
return false;
break;
case 0x08:
if (length != 2)
return false;
break;
case 0x0c:
if (length != 0 || value >= 256 || !m_eeprom_writable)
return false;
break;
case 0x14:
if (length != 6)
return false;
break;
case 0x16:
if (length != 8)
return false;
break;
default:
return false;
}
}
}
else
return false;
if (m_control.size() > length)
m_control.resize(length);
return true;
}
bool AX88772::ApplyControl(std::span<const u8> data)
{
const u8 type = m_setup[0], request = m_setup[1];
const u16 value = LE16(m_setup, 2), index = LE16(m_setup, 4);
if ((type & 0x80) != 0)
return true;
if ((type & 0x60) == 0)
{
switch (request)
{
case 1:
return value == 0; // CLEAR_FEATURE(ENDPOINT_HALT).
case 3:
return false; // Unsupported features must not silently succeed.
case 5:
if (value > 127)
return false;
m_address = static_cast<u8>(value);
INFO_LOG_FMT(IOS_USB, "AX88772 assigned USB address {}", value);
return true;
case 9:
if (value > 1)
return false;
m_configuration = static_cast<u8>(value);
INFO_LOG_FMT(IOS_USB, "AX88772 USB configuration {}", value);
m_link_event = true;
return true;
case 11:
return value == 0 && index == 0;
default:
return false;
}
}
switch (request)
{
case 0x06:
m_software_mii = true;
break;
case 0x08:
// PHY address/register are five-bit MDIO fields. An absent PHY does not
// make a valid USB vendor request stall; it simply cannot latch the write.
INFO_LOG_FMT(IOS_USB, "AX88772 MDIO write PHY={:#04x} reg={:#04x} value={:#06x}", value, index,
LE16(data));
if ((value & 0x1f) != 0x10)
break;
m_phy[index & 0x1f] = LE16(data);
// Autonegotiation restart and PHY reset commands self-clear.
if ((index & 0x1f) == 0)
{
m_phy[0] &= ~0x8200;
m_link_event = true;
}
break;
case 0x0a:
m_software_mii = false;
break;
case 0x0c:
m_eeprom[value] = index;
break;
case 0x0d:
m_eeprom_writable = true;
break;
case 0x0e:
m_eeprom_writable = false;
break;
case 0x10:
if (m_rx_control != value)
INFO_LOG_FMT(IOS_USB, "AX88772 RX control = {:#06x}", value);
m_rx_control = value;
break;
case 0x12:
m_ipg = {static_cast<u8>(value), static_cast<u8>(value >> 8), static_cast<u8>(index)};
break;
case 0x14:
std::copy(data.begin(), data.end(), m_mac.begin());
break;
case 0x16:
std::copy(data.begin(), data.end(), m_multicast.begin());
break;
case 0x1b:
if (m_medium != value)
INFO_LOG_FMT(IOS_USB, "AX88772 medium = {:#06x}, link {}", value, LinkUp());
m_medium = value;
break;
case 0x1d:
m_monitor = static_cast<u8>(value);
break;
case 0x1f:
m_gpio = value;
break;
case 0x20:
m_software_reset = static_cast<u8>(value);
if (value & 1)
{
m_rx.clear();
m_rx_offset = 0;
}
if (value & 2)
m_tx.clear();
m_link_event = true;
break;
case 0x22:
m_phy_select = static_cast<u8>(value);
break;
default:
return false;
}
return true;
}
AX88772::Result AX88772::BulkOut(std::span<const u8> data)
{
// A frame may span TDs. AX framing is length + one's complement, little-endian.
if (m_tx.size() + data.size() > 65536)
{
m_tx.clear();
return Result::Stalled;
}
m_tx.insert(m_tx.end(), data.begin(), data.end());
size_t consumed = 0;
while (m_tx.size() - consumed >= 4)
{
const u16 length = LE16(m_tx, consumed);
if (static_cast<u16>(length ^ LE16(m_tx, consumed + 2)) != 0xffff || length > MAX_FRAME)
{
m_tx.clear();
return Result::Stalled;
}
const size_t record_size = 4 + length;
if (m_tx.size() - consumed < record_size)
break;
if (length >= 14 && LinkUp() && m_transmit)
m_transmit(std::span(m_tx).subspan(consumed + 4, length));
consumed += record_size;
}
m_tx.erase(m_tx.begin(), m_tx.begin() + consumed);
return Result::Completed;
}
void AX88772::ReceiveFrame(std::span<const u8> frame)
{
if (!LinkUp() || (m_rx_control & 0x80) == 0 || (m_medium & 0x100) == 0 || frame.size() < 14 ||
frame.size() > MAX_FRAME || m_rx.size() >= MAX_QUEUE)
return;
const bool broadcast =
std::all_of(frame.begin(), frame.begin() + 6, [](u8 b) { return b == 0xff; });
const bool multicast = (frame[0] & 1) != 0;
if ((m_rx_control & 1) == 0 && !std::equal(m_mac.begin(), m_mac.end(), frame.begin()) &&
!(broadcast && (m_rx_control & 8)) && !(multicast && !broadcast && (m_rx_control & 0x12)))
return;
const u16 length = static_cast<u16>(frame.size());
auto packet = Word(length);
auto inverse = Word(static_cast<u16>(~length));
packet.insert(packet.end(), inverse.begin(), inverse.end());
packet.insert(packet.end(), frame.begin(), frame.end());
if (length & 1)
packet.push_back(0);
m_rx.push_back(std::move(packet));
}
AX88772::Result AX88772::Transfer(u8 endpoint, u8 direction, std::span<u8> buffer, size_t* actual)
{
*actual = 0;
if (direction > 2)
return Result::Stalled;
if (endpoint == 0)
{
if (direction == 0)
{
if (buffer.size() != 8)
return Result::Stalled;
std::copy(buffer.begin(), buffer.end(), m_setup.begin());
m_control_valid = true;
m_control_stalled = !PrepareControl();
if (m_control_log_count++ < 128)
INFO_LOG_FMT(IOS_USB,
"AX88772 control {:#04x}/{:#04x} value={:#06x} index={:#06x} "
"length={} response={:02x}",
m_setup[0], m_setup[1], LE16(m_setup, 2), LE16(m_setup, 4), LE16(m_setup, 6),
fmt::join(m_control, " "));
if (m_control_stalled)
WARN_LOG_FMT(
IOS_USB,
"AX88772 unsupported control {:#04x}/{:#04x} value={:#06x} index={:#06x} length={}",
m_setup[0], m_setup[1], LE16(m_setup, 2), LE16(m_setup, 4), LE16(m_setup, 6));
*actual = 8;
return m_control_stalled ? Result::Stalled : Result::Completed;
}
if (!m_control_valid || m_control_stalled)
return Result::Stalled;
const bool read = (m_setup[0] & 0x80) != 0;
if (buffer.empty())
{
if (direction != (read ? 1 : 2) || (!read && m_out.size() != LE16(m_setup, 6)))
return Result::Stalled;
const bool applied = ApplyControl(m_out);
m_control_valid = false;
return applied ? Result::Completed : Result::Stalled;
}
if (direction == 2 && read)
{
*actual = std::min(buffer.size(), m_control.size() - m_control_offset);
std::copy_n(m_control.begin() + m_control_offset, *actual, buffer.begin());
m_control_offset += *actual;
}
else if (direction == 1 && !read && m_out.size() + buffer.size() <= LE16(m_setup, 6))
{
m_out.insert(m_out.end(), buffer.begin(), buffer.end());
*actual = buffer.size();
}
else
return Result::Stalled;
return Result::Completed;
}
if (m_configuration == 0)
return Result::Stalled;
if (endpoint == 1 && direction == 2)
{
if (!m_link_event || m_software_mii)
return Result::Pending;
// Datasheet 4.1.4/6.3: the last four bytes are PHY register snapshots,
// selected by EEPROM word 0x0f, not unused padding.
const u16 first = ReadPHY((m_eeprom[0x0f] >> 8) & 0x1f);
const u16 second = ReadPHY(m_eeprom[0x0f] & 0x1f);
const std::array<u8, 8> event{0xa1,
0,
static_cast<u8>(LinkUp() ? 1 : 0),
0,
static_cast<u8>(first),
static_cast<u8>(first >> 8),
static_cast<u8>(second),
static_cast<u8>(second >> 8)};
*actual = std::min(buffer.size(), event.size());
std::copy_n(event.begin(), *actual, buffer.begin());
if (*actual == event.size())
{
m_link_event = false;
m_status_frames_remaining = INTERRUPT_INTERVAL_MS;
if (++m_interrupt_reports <= 8)
INFO_LOG_FMT(IOS_USB, "AX88772 status report: link={} PHY={:#06x}/{:#06x}", LinkUp(), first,
second);
}
return Result::Completed;
}
if (endpoint == 2 && direction == 2)
{
if (m_rx.empty())
return Result::Pending;
auto& packet = m_rx.front();
*actual = std::min(buffer.size(), packet.size() - m_rx_offset);
std::copy_n(packet.begin() + m_rx_offset, *actual, buffer.begin());
m_rx_offset += *actual;
if (m_rx_offset == packet.size())
{
m_rx.pop_front();
m_rx_offset = 0;
}
return Result::Completed;
}
if (endpoint == 3 && direction == 1)
{
const auto result = BulkOut(buffer);
if (result == Result::Completed)
*actual = buffer.size();
return result;
}
return Result::Stalled;
}
} // namespace IOS::LLE
+81
View File
@@ -0,0 +1,81 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <deque>
#include <functional>
#include <span>
#include <vector>
#include "Common/CommonTypes.h"
class PointerWrap;
namespace IOS::LLE
{
// USB full-speed AX88772A. IOS still executes its own USB, Ethernet and IP drivers.
// The backend exchanges Ethernet frames only; it never accesses guest memory or IOS IPC.
class AX88772
{
public:
enum class Result
{
Completed,
Pending,
Stalled
};
using Transmit = std::function<void(std::span<const u8>)>;
explicit AX88772(Transmit transmit);
void Reset();
void DoState(PointerWrap& p);
void SetLink(bool connected);
// One emulated full-speed USB frame (1 ms), not a host wall-clock tick.
void AdvanceUSBFrame();
void ReceiveFrame(std::span<const u8> frame);
Result Transfer(u8 endpoint, u8 direction, std::span<u8> buffer, size_t* actual);
u8 GetAddress() const { return m_address; }
private:
static constexpr u8 INTERRUPT_INTERVAL_MS = 10;
bool PrepareControl();
bool ApplyControl(std::span<const u8> data);
std::vector<u8> Descriptor(u16 value) const;
u16 ReadPHY(u16 reg) const;
bool LinkUp() const;
Result BulkOut(std::span<const u8> data);
Transmit m_transmit;
std::array<u8, 6> m_mac{0x02, 0x44, 0x4f, 0x4c, 0x00, 0x01};
std::array<u16, 256> m_eeprom{};
std::array<u16, 32> m_phy{};
std::array<u8, 8> m_multicast{};
std::array<u8, 3> m_ipg{0x15, 0x0c, 0x12};
std::array<u8, 8> m_setup{};
std::vector<u8> m_control;
std::vector<u8> m_out;
std::vector<u8> m_tx;
std::deque<std::vector<u8>> m_rx;
size_t m_control_offset = 0;
size_t m_rx_offset = 0;
u16 m_rx_control = 0;
u16 m_medium = 0;
u16 m_gpio = 0;
u8 m_monitor = 0;
u8 m_phy_select = 1;
u8 m_software_reset = 0x28;
u8 m_address = 0;
u8 m_configuration = 0;
bool m_control_valid = false;
bool m_control_stalled = false;
bool m_software_mii = false;
bool m_eeprom_writable = false;
bool m_connected = false;
bool m_link_event = true;
u8 m_status_frames_remaining = 0;
// Diagnostics only; not part of guest-visible/savestate state.
u32 m_control_log_count = 0;
u64 m_interrupt_reports = 0;
};
} // namespace IOS::LLE
@@ -0,0 +1,320 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/IOS/Starlet/SlirpNetwork.h"
#include <algorithm>
#include <cerrno>
#include <filesystem>
#include <limits>
#include <vector>
#define LIBSLIRP_STATIC
#include "libslirp.h"
#ifndef _WIN32
#include <poll.h>
#endif
#include "Common/DynamicLibrary.h"
#include "Common/Logging/Log.h"
#include "Common/Network.h"
#include "Common/StringUtil.h"
namespace IOS::LLE
{
namespace
{
#ifdef _WIN32
// WSAPoll rejects POLLPRI with WSAEINVAL, failing the entire poll array. Winsock
// uses POLLRDBAND for urgent data, and POLLIN combines normal and band events.
// Keep them distinct when translating to libslirp's IN/PRI flags.
constexpr short NATIVE_POLL_READ = POLLRDNORM;
constexpr short NATIVE_POLL_PRIORITY = POLLRDBAND;
#else
constexpr short NATIVE_POLL_READ = POLLIN;
constexpr short NATIVE_POLL_PRIORITY = POLLPRI;
#endif
// Header-only diagnostics: no HTTP contents, DNS names or other guest payloads.
void LogNetworkFrame(std::span<const u8> frame, const char* direction, u64 count)
{
if (count > 128 || frame.size() < 34 || frame[12] != 8 || frame[13] != 0 || (frame[14] >> 4) != 4)
return;
const auto ip = frame.subspan(14);
const auto word = [](std::span<const u8> bytes, size_t offset) {
return (u16(bytes[offset]) << 8) | bytes[offset + 1];
};
const auto dword = [&](std::span<const u8> bytes, size_t offset) {
return (u32(word(bytes, offset)) << 16) | word(bytes, offset + 2);
};
const size_t header_size = (ip[0] & 15) * 4;
const size_t total_size = word(ip, 2);
if (header_size < 20 || header_size > total_size || total_size > ip.size())
return;
const auto payload = ip.subspan(header_size, total_size - header_size);
std::string transport;
if ((word(ip, 6) & 0x1fff) != 0)
transport = "fragment";
else if (ip[9] == 6 && payload.size() >= 20)
transport = fmt::format("TCP {}->{} flags={:#04x} seq={} ack={} win={} header={}",
word(payload, 0), word(payload, 2), payload[13], dword(payload, 4),
dword(payload, 8), word(payload, 14), (payload[12] >> 4) * 4);
else if (ip[9] == 17 && payload.size() >= 8)
transport =
fmt::format("UDP {}->{} length={}", word(payload, 0), word(payload, 2), word(payload, 4));
else if (ip[9] == 1 && payload.size() >= 4)
transport = fmt::format("ICMP type={} code={}", payload[0], payload[1]);
INFO_LOG_FMT(IOS_NET,
"Starlet NAT {} #{} IPv4 {}.{}.{}.{} -> {}.{}.{}.{} proto={} bytes={} "
"IP-checksum={} {}",
direction, count, ip[12], ip[13], ip[14], ip[15], ip[16], ip[17], ip[18], ip[19],
ip[9], total_size,
Common::ComputeNetworkChecksum(ip.data(), static_cast<u16>(header_size)) == 0,
transport);
}
} // namespace
short SlirpSocketPolling::ToNativeEvents(int events)
{
short requested = 0;
if (events & SLIRP_POLL_IN)
requested |= NATIVE_POLL_READ;
if (events & SLIRP_POLL_OUT)
requested |= POLLOUT;
if (events & SLIRP_POLL_PRI)
requested |= NATIVE_POLL_PRIORITY;
return requested;
}
int SlirpSocketPolling::ToSlirpEvents(short events)
{
int ready = 0;
if (events & NATIVE_POLL_READ)
ready |= SLIRP_POLL_IN;
if (events & POLLOUT)
ready |= SLIRP_POLL_OUT;
if (events & NATIVE_POLL_PRIORITY)
ready |= SLIRP_POLL_PRI;
if (events & (POLLERR | POLLNVAL))
ready |= SLIRP_POLL_ERR;
if (events & POLLHUP)
ready |= SLIRP_POLL_HUP;
return ready;
}
struct SlirpNetwork::Impl
{
Common::DynamicLibrary library;
decltype(&slirp_new) create = nullptr;
decltype(&slirp_cleanup) cleanup = nullptr;
decltype(&slirp_input) input = nullptr;
decltype(&slirp_pollfds_fill_socket) fill = nullptr;
decltype(&slirp_pollfds_poll) poll = nullptr;
decltype(&slirp_version_string) version = nullptr;
Slirp* slirp = nullptr;
SlirpCb callbacks{};
Receive receive;
s64 now_ns = 0;
u64 last_arm_cycles = 0;
u64 elapsed_arm_cycles = 0;
u64 tx_frames = 0;
u64 rx_frames = 0;
u64 poll_errors = 0;
#ifdef _WIN32
bool winsock = false;
std::vector<WSAPOLLFD> sockets;
#else
std::vector<pollfd> sockets;
#endif
struct Timer
{
SlirpTimerCb callback;
void* opaque;
s64 deadline = std::numeric_limits<s64>::max();
};
std::vector<std::unique_ptr<Timer>> timers;
~Impl()
{
if (slirp)
cleanup(slirp);
#ifdef _WIN32
if (winsock)
WSACleanup();
#endif
}
};
SlirpNetwork::SlirpNetwork() = default;
SlirpNetwork::~SlirpNetwork() = default;
bool SlirpNetwork::Start(const std::string& directory, Receive receive, std::string* error)
{
m_impl.reset();
auto impl = std::make_unique<Impl>();
const auto fail = [&](const std::string& message) {
if (error)
*error = message;
return false;
};
#ifdef _WIN32
WSADATA wsadata{};
if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
return fail("Could not initialize Winsock for the Ethernet NAT backend");
impl->winsock = true;
// Search dependencies alongside this exact DLL, not via a modified process/global PATH.
const auto path = std::filesystem::path(UTF8ToWString(directory + "/libslirp-0.dll"))
.lexically_normal()
.wstring();
impl->library = LoadLibraryExW(
path.c_str(), nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
if (!impl->library.IsOpen())
return fail(fmt::format("Could not load libslirp NAT runtime (Windows error {}): {}. "
"Run Setup-Wii-LLE-Network.ps1 for this build.",
GetLastError(), WStringToUTF8(path)));
#elif defined(__APPLE__)
impl->library.Open((directory + "/libslirp.0.dylib").c_str());
#else
impl->library.Open((directory + "/libslirp.so.0").c_str());
#endif
if (!impl->library.IsOpen())
return fail("libslirp NAT runtime is missing; run Setup-Wii-LLE-Network.ps1 for this build");
if (!impl->library.GetSymbol("slirp_new", &impl->create) ||
!impl->library.GetSymbol("slirp_cleanup", &impl->cleanup) ||
!impl->library.GetSymbol("slirp_input", &impl->input) ||
!impl->library.GetSymbol("slirp_pollfds_fill_socket", &impl->fill) ||
!impl->library.GetSymbol("slirp_pollfds_poll", &impl->poll) ||
!impl->library.GetSymbol("slirp_version_string", &impl->version))
return fail("The Ethernet NAT backend requires libslirp 4.9 or newer");
impl->receive = std::move(receive);
auto& cb = impl->callbacks;
cb.send_packet = [](const void* data, size_t length, void* opaque) -> slirp_ssize_t {
auto& self = *static_cast<Impl*>(opaque);
if (length >= 14 && length <= 1518 && self.receive)
{
if (++self.rx_frames <= 8)
INFO_LOG_FMT(IOS_NET, "Starlet NAT -> USB Ethernet: frame {} ({} bytes)", self.rx_frames,
length);
LogNetworkFrame({static_cast<const u8*>(data), length}, "RX", self.rx_frames);
self.receive({static_cast<const u8*>(data), length});
}
return static_cast<slirp_ssize_t>(length);
};
cb.guest_error = [](const char* message, void*) {
WARN_LOG_FMT(IOS_NET, "Starlet NAT: {}", message);
};
cb.clock_get_ns = [](void* opaque) -> int64_t { return static_cast<Impl*>(opaque)->now_ns; };
cb.timer_new = [](SlirpTimerCb callback, void* callback_opaque, void* opaque) -> void* {
auto& timers = static_cast<Impl*>(opaque)->timers;
timers.push_back(std::make_unique<Impl::Timer>(Impl::Timer{callback, callback_opaque}));
return timers.back().get();
};
cb.timer_free = [](void* timer, void* opaque) {
auto& timers = static_cast<Impl*>(opaque)->timers;
std::erase_if(timers, [timer](const auto& entry) { return entry.get() == timer; });
};
cb.timer_mod = [](void* timer, int64_t expiry, void*) {
static_cast<Impl::Timer*>(timer)->deadline = expiry;
};
cb.register_poll_socket = [](slirp_os_socket, void*) {};
cb.unregister_poll_socket = [](slirp_os_socket, void*) {};
cb.notify = [](void*) {}; // Nonblocking polls at emulated USB frame boundaries.
SlirpConfig config{};
config.version = 6;
config.in_enabled = true;
config.vnetwork.s_addr = htonl(0x0a000200);
config.vnetmask.s_addr = htonl(0xffffff00);
config.vhost.s_addr = htonl(0x0a000202);
config.vdhcp_start.s_addr = htonl(0x0a00020f);
config.vnameserver.s_addr = htonl(0x0a000203);
config.vhostname = "dolphin-wii";
config.if_mtu = config.if_mru = 1500;
config.disable_host_loopback = true;
// No host services, TFTP file sharing, command forwarding or inbound port mappings.
config.enable_emu = false;
impl->slirp = impl->create(&config, &cb, impl.get());
if (!impl->slirp)
return fail("libslirp could not create the Ethernet NAT network");
INFO_LOG_FMT(IOS_NET, "Starlet Ethernet NAT initialized: libslirp {}, subnet 10.0.2.0/24",
impl->version());
m_impl = std::move(impl);
return true;
}
void SlirpNetwork::Input(std::span<const u8> frame)
{
if (m_impl && frame.size() >= 14 && frame.size() <= 1518)
{
if (++m_impl->tx_frames <= 8)
INFO_LOG_FMT(IOS_NET, "Starlet USB Ethernet -> NAT: frame {} ({} bytes, type {:#06x})",
m_impl->tx_frames, frame.size(), (u16(frame[12]) << 8) | frame[13]);
LogNetworkFrame(frame, "TX", m_impl->tx_frames);
m_impl->input(m_impl->slirp, frame.data(), static_cast<int>(frame.size()));
}
}
void SlirpNetwork::Poll(u64 arm_cycles)
{
if (!m_impl)
return;
auto& self = *m_impl;
// A console reset restarts the ARM cycle counter, not the NAT's monotonic clock.
self.elapsed_arm_cycles +=
arm_cycles >= self.last_arm_cycles ? arm_cycles - self.last_arm_cycles : arm_cycles;
self.last_arm_cycles = arm_cycles;
self.now_ns = static_cast<s64>((self.elapsed_arm_cycles / 243000000) * 1000000000 +
(self.elapsed_arm_cycles % 243000000) * 1000000000 / 243000000);
self.sockets.clear();
uint32_t timeout = 0;
self.fill(
self.slirp, &timeout,
[](slirp_os_socket socket, int events, void* opaque) -> int {
auto& sockets = static_cast<Impl*>(opaque)->sockets;
sockets.push_back({socket, SlirpSocketPolling::ToNativeEvents(events), 0});
return static_cast<int>(sockets.size() - 1);
},
&self);
int result = 0;
if (!self.sockets.empty())
{
#ifdef _WIN32
result = WSAPoll(self.sockets.data(), static_cast<ULONG>(self.sockets.size()), 0);
#else
result = ::poll(self.sockets.data(), self.sockets.size(), 0);
#endif
if (result < 0)
{
#ifdef _WIN32
const int error = WSAGetLastError();
#else
const int error = errno;
#endif
if (++self.poll_errors <= 8)
WARN_LOG_FMT(IOS_NET, "Starlet NAT socket poll failed: error {} ({} sockets)", error,
self.sockets.size());
}
}
self.poll(
self.slirp, result < 0,
[](int index, void* opaque) -> int {
const auto& sockets = static_cast<Impl*>(opaque)->sockets;
if (index < 0 || static_cast<size_t>(index) >= sockets.size())
return 0;
return SlirpSocketPolling::ToSlirpEvents(sockets[index].revents);
},
&self);
for (size_t count = 0; count < 64; ++count)
{
const auto timer = std::find_if(self.timers.begin(), self.timers.end(), [&](const auto& entry) {
return entry->deadline <= self.now_ns / 1000000;
});
if (timer == self.timers.end())
break;
const auto callback = (*timer)->callback;
void* opaque = (*timer)->opaque;
(*timer)->deadline = std::numeric_limits<s64>::max();
callback(opaque);
}
}
} // namespace IOS::LLE
@@ -0,0 +1,38 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <functional>
#include <memory>
#include <span>
#include <string>
#include "Common/CommonTypes.h"
namespace IOS::LLE
{
// Platform poll flags differ between Winsock and POSIX. Kept separately from
// the NAT lifetime so the real host polling behavior can be regression-tested.
namespace SlirpSocketPolling
{
short ToNativeEvents(int events);
int ToSlirpEvents(short events);
} // namespace SlirpSocketPolling
// Nonblocking Ethernet NAT backend. All calls and callbacks stay on the emulation thread.
class SlirpNetwork
{
public:
using Receive = std::function<void(std::span<const u8>)>;
SlirpNetwork();
~SlirpNetwork();
bool Start(const std::string& directory, Receive receive, std::string* error);
void Input(std::span<const u8> frame);
void Poll(u64 arm_cycles);
private:
struct Impl;
std::unique_ptr<Impl> m_impl;
};
} // namespace IOS::LLE
+189 -30
View File
@@ -462,6 +462,8 @@ StarletMemory::StarletMemory(Core::System& system) : m_system(system)
bool StarletMemory::Init(const std::string& dump_directory, std::string* error)
{
m_ethernet_network.reset();
m_ethernet.reset();
m_nand_journal.reset();
m_nand_overlay.clear();
const std::string boot_path = PathInDirectory(dump_directory, "boot0.bin");
@@ -554,6 +556,11 @@ bool StarletMemory::Init(const std::string& dump_directory, std::string* error)
// replacing them immediately after reading BT.DINF leaves a short-lived
// callback pointing at a destroyed device.
m_initialized = false;
if (Config::Get(Config::MAIN_WII_LLE_ETHERNET))
{
if (!EnableEthernetNAT(File::GetExeDirectory() + "/Network", error))
return false;
}
InitSDCard();
Reset();
if (!InstallEmulatedWiimotePairings())
@@ -569,6 +576,30 @@ bool StarletMemory::Init(const std::string& dump_directory, std::string* error)
return true;
}
bool StarletMemory::EnableEthernetNAT(const std::string& runtime_directory, std::string* error)
{
m_ethernet_network.reset();
m_ethernet = std::make_unique<AX88772>([this](std::span<const u8> frame) {
if (m_ethernet_network)
m_ethernet_network->Input(frame);
});
m_ethernet_network = std::make_unique<SlirpNetwork>();
if (!m_ethernet_network->Start(
runtime_directory, [this](std::span<const u8> frame) { m_ethernet->ReceiveFrame(frame); },
error))
{
m_ethernet_network.reset();
m_ethernet.reset();
return false;
}
m_ethernet->SetLink(true);
WriteRegister(EHCI_BASE + EHCI_PORT_STATUS_1,
EHCI_PORT_CONNECT_STATUS | EHCI_PORT_CONNECT_CHANGE | EHCI_PORT_POWER | (2U << 10));
UpdateEthernetPortRouting();
INFO_LOG_FMT(IOS_USB, "Starlet external USB port 1: virtual AX88772A, full-speed, Ethernet NAT");
return true;
}
void StarletMemory::InitSDCard()
{
m_sd_card.Close();
@@ -599,6 +630,8 @@ void StarletMemory::InitSDCard()
void StarletMemory::Reset()
{
if (m_ethernet)
m_ethernet->Reset();
const std::lock_guard wifi_sdio_lock(m_wifi_sdio_register_mutex);
for (auto& wiimote : m_wiimotes)
{
@@ -785,6 +818,31 @@ void StarletMemory::DoState(PointerWrap& p)
p.Do(m_initialized);
p.Do(m_boot0_mapped);
p.Do(m_sram_split_mode);
bool ethernet_enabled = m_ethernet != nullptr;
p.Do(ethernet_enabled);
if (p.IsReadMode() && ethernet_enabled != (m_ethernet != nullptr))
{
PanicAlertFmtT("This savestate requires the same Wii LLE Ethernet setting. Aborting load.");
p.SetVerifyMode();
return;
}
if (m_ethernet)
{
m_ethernet->DoState(p);
if (p.IsReadMode())
{
std::string error;
if (!m_ethernet_network->Start(
File::GetExeDirectory() + "/Network",
[this](std::span<const u8> frame) { m_ethernet->ReceiveFrame(frame); }, &error))
{
PanicAlertFmtT("Could not restart Ethernet NAT: {0}", error);
p.SetVerifyMode();
return;
}
WARN_LOG_FMT(IOS_NET, "Restored AX88772 state; host TCP/UDP connections were reset");
}
}
if (p.IsReadMode() && m_nand_journal && !m_nand_journal->Append(m_nand_overlay, true))
{
PanicAlertFmtT("Could not persist the NAND restored from the savestate. "
@@ -1008,6 +1066,32 @@ void StarletMemory::WriteMapped8(u32 address, u8 value)
std::memory_order_relaxed);
}
u8 StarletMemory::ReadDMA8(u32 address) const
{
// HW_SRNPROT.SM swaps the CPU's SRAM apertures, not the AHB masters' physical
// banks. Firmware (e.g. MINI's dma_addr) already removes that swap before
// programming a DMA register. Applying GetSRAMOffset again can select the
// wrong bank or discard IOS's AES output into the CPU's split-mode hole.
if (address >= SRAM_BASE && address < SRAM_BASE + SRAM_WINDOW_SIZE)
{
const u32 offset = address - SRAM_BASE;
return offset < SRAM_SIZE ? m_sram[offset] : 0;
}
return ReadMapped8(address);
}
void StarletMemory::WriteDMA8(u32 address, u8 value)
{
if (address >= SRAM_BASE && address < SRAM_BASE + SRAM_WINDOW_SIZE)
{
const u32 offset = address - SRAM_BASE;
if (offset < SRAM_SIZE)
m_sram[offset] = value;
return;
}
WriteMapped8(address, value);
}
void StarletMemory::ClearRegisterCache()
{
for (auto& entry : m_register_cache)
@@ -1064,9 +1148,35 @@ void StarletMemory::ResetEHCIController(bool preserve_phy_registers)
m_ehci_running = false;
WriteRegister(EHCI_BASE + EHCI_USB_COMMAND, EHCI_COMMAND_HOLLYWOOD_RESET_VALUE);
WriteRegister(EHCI_BASE + EHCI_USB_STATUS, EHCI_STATUS_HALTED);
if (m_ethernet)
WriteRegister(EHCI_BASE + EHCI_PORT_STATUS_1, EHCI_PORT_CONNECT_STATUS |
EHCI_PORT_CONNECT_CHANGE | EHCI_PORT_POWER |
(2U << 10));
UpdateEthernetPortRouting();
UpdateEHCIInterrupt();
}
void StarletMemory::UpdateEthernetPortRouting()
{
if (!m_ethernet)
return;
const u32 ehci_port = ReadRegister(EHCI_BASE + EHCI_PORT_STATUS_1);
const bool companion = (ReadRegister(EHCI_BASE + EHCI_CONFIGURED_FLAG) & 1) == 0 ||
(ehci_port & EHCI_PORT_OWNER) != 0;
const u32 address = OHCI_BASES[0] + OHCI_RH_PORT_STATUS_1;
const u32 old = ReadRegister(address);
u32 port = old;
if (companion && (ehci_port & EHCI_PORT_POWER) != 0)
port |= OHCI_PORT_CURRENT_CONNECT_STATUS | OHCI_PORT_POWER_STATUS;
else
port &= ~(OHCI_PORT_CURRENT_CONNECT_STATUS | OHCI_PORT_ENABLE_STATUS);
if ((old ^ port) & OHCI_PORT_CURRENT_CONNECT_STATUS)
port |= OHCI_PORT_CONNECT_STATUS_CHANGE;
WriteRegister(address, port);
if (port & OHCI_PORT_CHANGE_MASK)
SetOHCIInterruptStatus(0, OHCI_INTERRUPT_ROOT_HUB_STATUS_CHANGE);
}
void StarletMemory::SetEHCIInterruptStatus(u32 status)
{
WriteRegister(EHCI_BASE + EHCI_USB_STATUS,
@@ -1142,6 +1252,10 @@ void StarletMemory::HandleEHCIWrite(u32 address)
case EHCI_ASYNC_LIST_ADDRESS:
WriteRegister(address, value & 0xffffffe0);
break;
case EHCI_CONFIGURED_FLAG:
WriteRegister(address, value & 1);
UpdateEthernetPortRouting();
break;
case EHCI_PORT_STATUS_1:
case EHCI_PORT_STATUS_2:
{
@@ -1152,7 +1266,18 @@ void StarletMemory::HandleEHCIWrite(u32 address)
port &= ~(EHCI_PORT_POWER | EHCI_PORT_OWNER | EHCI_PORT_SUSPEND | EHCI_PORT_RESET);
port |= value & (EHCI_PORT_POWER | EHCI_PORT_OWNER | EHCI_PORT_SUSPEND | EHCI_PORT_RESET);
port &= ~(EHCI_PORT_CONNECT_STATUS | EHCI_PORT_ENABLE);
if (m_ethernet && offset == EHCI_PORT_STATUS_1)
{
// The full-speed device belongs to OHCI0 after the EHCI owner handoff.
port |= EHCI_PORT_CONNECT_STATUS | (2U << 10);
if (value & EHCI_PORT_RESET)
{
port = (port | EHCI_PORT_OWNER) & ~EHCI_PORT_RESET;
m_ethernet->Reset();
}
}
WriteRegister(address, port);
UpdateEthernetPortRouting();
UpdateEHCIInterrupt();
break;
}
@@ -1262,6 +1387,8 @@ void StarletMemory::ResetOHCIController(size_t controller)
// Bluetooth daughter board is permanently wired to OHCI1; the two OHCI0
// companion ports remain removable.
WriteRegister(base + OHCI_RH_DESCRIPTOR_B, controller == 1 ? 1U << 1 : 0);
if (controller == 0)
UpdateEthernetPortRouting();
if (controller == 1)
{
WriteRegister(base + OHCI_RH_PORT_STATUS_1, OHCI_PORT_CURRENT_CONNECT_STATUS |
@@ -1393,6 +1520,8 @@ void StarletMemory::HandleOHCIWrite(size_t controller, u32 address)
port |= OHCI_PORT_RESET_STATUS;
port &= ~(OHCI_PORT_ENABLE_STATUS | OHCI_PORT_SUSPEND_STATUS);
m_ohci_port_reset_frames[controller][port_index] = OHCI_PORT_RESET_FRAMES;
if (controller == 0 && port_index == 0 && m_ethernet)
m_ethernet->Reset();
if (controller == 1 && port_index == 0)
{
m_ohci1_device_address = 0;
@@ -1408,6 +1537,10 @@ void StarletMemory::HandleOHCIWrite(size_t controller, u32 address)
if ((value & (1U << 8)) != 0)
{
port |= OHCI_PORT_POWER_STATUS;
if (controller == 0 && port_index == 0 && m_ethernet &&
((ReadRegister(EHCI_BASE + EHCI_CONFIGURED_FLAG) & 1) == 0 ||
(ReadRegister(EHCI_BASE + EHCI_PORT_STATUS_1) & EHCI_PORT_OWNER) != 0))
port |= OHCI_PORT_CURRENT_CONNECT_STATUS | OHCI_PORT_CONNECT_STATUS_CHANGE;
if (controller == 1 && port_index == 0 && (port & OHCI_PORT_CURRENT_CONNECT_STATUS) == 0)
m_ohci1_attach_delay_frames = OHCI1_ATTACH_DELAY_FRAMES;
}
@@ -1446,6 +1579,10 @@ void StarletMemory::AdvanceOHCI(size_t controller, u64 cycles)
const u16 old_frame = static_cast<u16>(ReadRegister(base + OHCI_FRAME_NUMBER));
const u16 frame = static_cast<u16>(old_frame + 1);
WriteRegister(base + OHCI_FRAME_NUMBER, frame);
if (controller == 0 && m_ethernet)
m_ethernet->AdvanceUSBFrame();
if (controller == 0 && m_ethernet_network)
m_ethernet_network->Poll(m_arm_cycles);
for (size_t port_index = 0; port_index < m_ohci_port_reset_frames[controller].size();
++port_index)
@@ -1484,8 +1621,8 @@ void StarletMemory::AdvanceOHCI(size_t controller, u64 cycles)
const u32 hcca = ReadRegister(base + OHCI_HCCA);
if (hcca != 0)
{
WriteMapped8(hcca + 0x80, static_cast<u8>(frame));
WriteMapped8(hcca + 0x81, static_cast<u8>(frame >> 8));
WriteDMA8(hcca + 0x80, static_cast<u8>(frame));
WriteDMA8(hcca + 0x81, static_cast<u8>(frame >> 8));
}
ProcessOHCISchedules(controller);
SetOHCIInterruptStatus(controller, OHCI_INTERRUPT_START_OF_FRAME);
@@ -1496,17 +1633,17 @@ void StarletMemory::AdvanceOHCI(size_t controller, u64 cycles)
u32 StarletMemory::ReadOHCIMemory32(u32 address) const
{
return static_cast<u32>(ReadMapped8(address)) | static_cast<u32>(ReadMapped8(address + 1)) << 8 |
static_cast<u32>(ReadMapped8(address + 2)) << 16 |
static_cast<u32>(ReadMapped8(address + 3)) << 24;
return static_cast<u32>(ReadDMA8(address)) | static_cast<u32>(ReadDMA8(address + 1)) << 8 |
static_cast<u32>(ReadDMA8(address + 2)) << 16 |
static_cast<u32>(ReadDMA8(address + 3)) << 24;
}
void StarletMemory::WriteOHCIMemory32(u32 address, u32 value)
{
WriteMapped8(address, static_cast<u8>(value));
WriteMapped8(address + 1, static_cast<u8>(value >> 8));
WriteMapped8(address + 2, static_cast<u8>(value >> 16));
WriteMapped8(address + 3, static_cast<u8>(value >> 24));
WriteDMA8(address, static_cast<u8>(value));
WriteDMA8(address + 1, static_cast<u8>(value >> 8));
WriteDMA8(address + 2, static_cast<u8>(value >> 16));
WriteDMA8(address + 3, static_cast<u8>(value >> 24));
}
std::vector<u8> StarletMemory::ReadOHCIBuffer(u32 current_buffer, u32 buffer_end) const
@@ -1525,10 +1662,10 @@ std::vector<u8> StarletMemory::ReadOHCIBuffer(u32 current_buffer, u32 buffer_end
std::vector<u8> buffer(first_size + second_size);
for (size_t i = 0; i < first_size; ++i)
buffer[i] = ReadMapped8(current_buffer + static_cast<u32>(i));
buffer[i] = ReadDMA8(current_buffer + static_cast<u32>(i));
const u32 second_page = buffer_end & ~0xfffU;
for (size_t i = 0; i < second_size; ++i)
buffer[first_size + i] = ReadMapped8(second_page + static_cast<u32>(i));
buffer[first_size + i] = ReadDMA8(second_page + static_cast<u32>(i));
return buffer;
}
@@ -1542,14 +1679,14 @@ void StarletMemory::WriteOHCIBuffer(u32 current_buffer, u32 buffer_end, const u8
same_page ? buffer_end - current_buffer + 1 : 0x1000 - (current_buffer & 0xfff);
const size_t first_size = std::min(size, first_capacity);
for (size_t i = 0; i < first_size; ++i)
WriteMapped8(current_buffer + static_cast<u32>(i), data[i]);
WriteDMA8(current_buffer + static_cast<u32>(i), data[i]);
if (size > first_size)
{
const u32 second_page = buffer_end & ~0xfffU;
const size_t second_size =
std::min(size - first_size, static_cast<size_t>((buffer_end & 0xfff) + 1));
for (size_t i = 0; i < second_size; ++i)
WriteMapped8(second_page + static_cast<u32>(i), data[first_size + i]);
WriteDMA8(second_page + static_cast<u32>(i), data[first_size + i]);
}
}
@@ -2059,6 +2196,20 @@ StarletMemory::OHCITransferResult StarletMemory::ExecuteOHCITransfer(size_t cont
size_t* actual_length)
{
*actual_length = 0;
if (controller == 0 && m_ethernet)
{
const auto result = m_ethernet->Transfer(static_cast<u8>(endpoint), static_cast<u8>(direction),
*buffer, actual_length);
switch (result)
{
case AX88772::Result::Completed:
return OHCITransferResult::Completed;
case AX88772::Result::Pending:
return OHCITransferResult::Pending;
case AX88772::Result::Stalled:
return OHCITransferResult::Stalled;
}
}
if (controller != 1)
return OHCITransferResult::Stalled;
@@ -2244,7 +2395,14 @@ bool StarletMemory::ProcessOHCIEndpoint(size_t controller, u32 endpoint_address,
size_t actual_length = 0;
OHCITransferResult result = OHCITransferResult::Stalled;
if (controller == 1 && function_address == m_ohci1_device_address && direction != 3)
const bool ethernet_ready =
controller == 0 && m_ethernet &&
(ReadRegister(base + OHCI_RH_PORT_STATUS_1) &
(OHCI_PORT_CURRENT_CONNECT_STATUS | OHCI_PORT_ENABLE_STATUS | OHCI_PORT_SUSPEND_STATUS)) ==
(OHCI_PORT_CURRENT_CONNECT_STATUS | OHCI_PORT_ENABLE_STATUS);
const u32 device_address =
controller == 0 && m_ethernet ? m_ethernet->GetAddress() : m_ohci1_device_address;
if ((controller == 1 || ethernet_ready) && function_address == device_address && direction != 3)
result = ExecuteOHCITransfer(controller, endpoint, direction, &buffer, &actual_length);
else if (controller == 1 && direction != 3)
result = OHCITransferResult::Stalled;
@@ -2254,8 +2412,9 @@ bool StarletMemory::ProcessOHCIEndpoint(size_t controller, u32 endpoint_address,
u32 condition_code = OHCI_CC_NO_ERROR;
if (result == OHCITransferResult::Stalled)
condition_code = function_address == m_ohci1_device_address ? OHCI_CC_STALL :
OHCI_CC_DEVICE_NOT_RESPONDING;
condition_code = (controller == 1 || ethernet_ready) && function_address == device_address ?
OHCI_CC_STALL :
OHCI_CC_DEVICE_NOT_RESPONDING;
// OHCI uses the TD's current-buffer pointer to report how much of the
// requested buffer was left after a short packet. Clearing it means the
// entire buffer was transferred, which makes IOS pass the buffer capacity
@@ -2749,9 +2908,9 @@ void StarletMemory::ExecuteWiFiSDIOCommand(u32 command_and_mode)
for (u32 i = 0; i < size; ++i)
{
if (write)
WriteWiFiSDIOByte(function, register_address, ReadMapped8(dma_address + i));
WriteWiFiSDIOByte(function, register_address, ReadDMA8(dma_address + i));
else
WriteMapped8(dma_address + i, ReadWiFiSDIOByte(function, register_address));
WriteDMA8(dma_address + i, ReadWiFiSDIOByte(function, register_address));
if (increment)
register_address = (register_address + 1) & 0x1ffff;
}
@@ -2923,14 +3082,14 @@ bool StarletMemory::TransferSDCardBlocks(bool read, u32 argument, u32 block_size
if (!m_sd_card.ReadBytes(data.data(), data.size()))
return false;
for (size_t i = 0; i < data.size(); ++i)
WriteMapped8(dma_address + static_cast<u32>(i), data[i]);
WriteDMA8(dma_address + static_cast<u32>(i), data[i]);
}
else
{
if (!Config::Get(Config::MAIN_ALLOW_SD_WRITES))
return false;
for (size_t i = 0; i < data.size(); ++i)
data[i] = ReadMapped8(dma_address + static_cast<u32>(i));
data[i] = ReadDMA8(dma_address + static_cast<u32>(i));
if (!m_sd_card.WriteBytes(data.data(), data.size()))
return false;
}
@@ -2985,7 +3144,7 @@ void StarletMemory::ExecuteSDHCCommand(u32 command_and_mode)
}
constexpr std::array<u8, 8> scr = {0x02, 0x05, 0, 0, 0, 0, 0, 0};
for (size_t i = 0; i < scr.size(); ++i)
WriteMapped8(dma_address + static_cast<u32>(i), scr[i]);
WriteDMA8(dma_address + static_cast<u32>(i), scr[i]);
WriteRegister(SDHC_DMA_ADDRESS, dma_address + static_cast<u32>(scr.size()));
transfer_complete = true;
break;
@@ -4153,7 +4312,7 @@ bool StarletMemory::ReadNANDPage(u32 command)
u32 destination = data_address + i;
if (m_nand_read_column < NAND_PAGE_DATA_SIZE && raw_offset >= NAND_PAGE_DATA_SIZE)
destination = spare_address + raw_offset - NAND_PAGE_DATA_SIZE;
WriteMapped8(destination, raw[raw_offset]);
WriteDMA8(destination, raw[raw_offset]);
}
if (command & NAND_CTRL_ECC)
@@ -4161,7 +4320,7 @@ bool StarletMemory::ReadNANDPage(u32 command)
const auto ecc = CalculateNANDECC(raw.data());
const u32 calculated_ecc_address = spare_address ^ 0x40U;
for (u32 i = 0; i < ecc.size(); ++i)
WriteMapped8(calculated_ecc_address + i, ecc[i]);
WriteDMA8(calculated_ecc_address + i, ecc[i]);
}
return true;
}
@@ -4177,7 +4336,7 @@ bool StarletMemory::ReadNANDID(u32 command)
const u32 data_address = ReadRegister(NAND_DATA) & ~0xfU;
for (u32 i = 0; i < length; ++i)
WriteMapped8(data_address + i, NAND_CHIP_ID[i % NAND_CHIP_ID.size()]);
WriteDMA8(data_address + i, NAND_CHIP_ID[i % NAND_CHIP_ID.size()]);
return true;
}
@@ -4192,7 +4351,7 @@ bool StarletMemory::ReadNANDStatus(u32 command)
const u32 data_address = ReadRegister(NAND_DATA) & ~0xfU;
for (u32 i = 0; i < length; ++i)
WriteMapped8(data_address + i, m_nand_status);
WriteDMA8(data_address + i, m_nand_status);
return true;
}
@@ -4230,7 +4389,7 @@ bool StarletMemory::StageNANDProgram(u32 command, bool random_data_input)
u32 source = data_address + i;
if (column < NAND_PAGE_DATA_SIZE && raw_offset >= NAND_PAGE_DATA_SIZE)
source = spare_address + raw_offset - NAND_PAGE_DATA_SIZE;
m_nand_program_data[raw_offset] = ReadMapped8(source);
m_nand_program_data[raw_offset] = ReadDMA8(source);
}
if ((command & NAND_CTRL_ECC) != 0 && column == 0 && length >= NAND_PAGE_DATA_SIZE)
@@ -4238,7 +4397,7 @@ bool StarletMemory::StageNANDProgram(u32 command, bool random_data_input)
const auto ecc = CalculateNANDECC(m_nand_program_data.data());
const u32 calculated_ecc_address = spare_address ^ 0x40U;
for (u32 i = 0; i < ecc.size(); ++i)
WriteMapped8(calculated_ecc_address + i, ecc[i]);
WriteDMA8(calculated_ecc_address + i, ecc[i]);
m_nand_program_ecc_enabled = true;
}
return true;
@@ -4409,7 +4568,7 @@ void StarletMemory::ExecuteAESCommand(u32 command)
std::vector<u8> input(size);
std::vector<u8> output(size);
for (size_t i = 0; i < size; ++i)
input[i] = ReadMapped8(source + static_cast<u32>(i));
input[i] = ReadDMA8(source + static_cast<u32>(i));
std::array<u8, 16> next_iv{};
bool succeeded = true;
@@ -4430,7 +4589,7 @@ void StarletMemory::ExecuteAESCommand(u32 command)
if ((command & AES_CTRL_ENABLE) != 0)
m_aes_iv = next_iv;
for (size_t i = 0; i < size; ++i)
WriteMapped8(destination + static_cast<u32>(i), output[i]);
WriteDMA8(destination + static_cast<u32>(i), output[i]);
WriteRegister(AES_SRC, source + static_cast<u32>(size));
WriteRegister(AES_DEST, destination + static_cast<u32>(size));
}
@@ -4517,7 +4676,7 @@ void StarletMemory::ExecuteSHACommand(u32 command)
for (u32 i = 0; i < blocks; ++i)
{
for (u32 j = 0; j < block.size(); ++j)
block[j] = ReadMapped8(source + i * 64 + j);
block[j] = ReadDMA8(source + i * 64 + j);
CompressSHA1(block.data());
}
for (u32 i = 0; i < m_sha_state.size(); ++i)
@@ -17,7 +17,9 @@
#include "Common/CommonTypes.h"
#include "Common/IOFile.h"
#include "Core/IOS/Starlet/ARMCore.h"
#include "Core/IOS/Starlet/AX88772.h"
#include "Core/IOS/Starlet/NANDJournal.h"
#include "Core/IOS/Starlet/SlirpNetwork.h"
#include "Core/IOS/USB/Bluetooth/WiimoteDevice.h"
class PointerWrap;
@@ -74,6 +76,7 @@ public:
u64 GetCycles() const { return m_arm_cycles; }
std::optional<u32> TryReadBroadwayResetInstruction(u32 address) const;
void SetWiimoteSource(size_t index, WiimoteCommon::HIDWiimote* source);
bool EnableEthernetNAT(const std::string& runtime_directory, std::string* error);
private:
static constexpr u32 NAND_PAGE_DATA_SIZE = 0x800;
@@ -91,6 +94,8 @@ private:
u8 ReadMapped8(u32 address) const;
void WriteMapped8(u32 address, u8 value);
u8 ReadDMA8(u32 address) const;
void WriteDMA8(u32 address, u8 value);
bool IsBootROMAddress(u32 address) const;
u32 GetBootROMOffset(u32 address) const;
u32 GetSRAMOffset(u32 address) const;
@@ -127,6 +132,7 @@ private:
void SetEHCIInterruptStatus(u32 status);
void UpdateEHCIInterrupt();
void AdvanceEHCI(u64 cycles);
void UpdateEthernetPortRouting();
static std::optional<size_t> GetOHCIControllerIndex(u32 address);
u32 ReadOHCIRegister(size_t controller, u32 address) const;
void HandleOHCIWrite(size_t controller, u32 address);
@@ -309,5 +315,7 @@ private:
bool m_sram_split_mode = false;
// Host persistence is not serialized; savestates contain the complete overlay above.
std::unique_ptr<NANDJournal> m_nand_journal;
std::unique_ptr<AX88772> m_ethernet;
std::unique_ptr<SlirpNetwork> m_ethernet_network;
};
} // namespace IOS::LLE
+1 -1
View File
@@ -97,7 +97,7 @@ struct CompressAndDumpStateArgs
static Common::WorkQueueThreadSP<CompressAndDumpStateArgs> s_compress_and_dump_thread;
// Don't forget to increase this after doing changes on the savestate system
constexpr u32 STATE_VERSION = 193; // Starlet HW_TIMER counter offset.
constexpr u32 STATE_VERSION = 196; // Correct Hollywood DI/reset pending interrupt bit mapping.
// Increase this if the StateExtendedHeader definition changes
constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217
+2
View File
@@ -16,6 +16,8 @@ add_dolphin_test(ESFormatsTest IOS/ES/FormatsTest.cpp)
add_dolphin_test(StarletARMCoreTest IOS/Starlet/ARMCoreTest.cpp)
add_dolphin_test(StarletNANDJournalTest IOS/Starlet/NANDJournalTest.cpp)
add_dolphin_test(StarletEthernetTest IOS/Starlet/AX88772Test.cpp)
target_include_directories(StarletEthernetTest PRIVATE ${PROJECT_SOURCE_DIR}/Externals/libslirp/include)
add_dolphin_test(FileSystemTest IOS/FS/FileSystemTest.cpp)
@@ -47,6 +47,8 @@ public:
u16 Read16(u32 address) override
{
++m_read16_count;
if (address >= MMIO_WORD_ADDRESS && address <= MMIO_WORD_ADDRESS + 2)
return static_cast<u16>(m_mmio_word >> (16 - (address & 3) * 8));
const size_t offset = ToOffset(address);
EXPECT_LT(offset + 1, m_memory.size());
if (offset + 1 >= m_memory.size())
@@ -333,6 +335,137 @@ TEST(StarletSRAM, WideAccessesPreserveAliasesSplitMappingAndBoot0Protection)
EXPECT_EQ(memory.Read32(sram_low + 0x20), 0xaabbccddu);
}
TEST(StarletDMA, AESUsesPhysicalSRAMBanksRegardlessOfCPUSplit)
{
// NIST SP 800-38A F.2.1, AES-128 CBC block 1. Exercise the actual MMIO engine,
// including IOS's in-place SRAM-stack operation (CPU fffff080 -> DMA 0d40f080).
constexpr std::array<u32, 4> key = {0x2b7e1516, 0x28aed2a6, 0xabf71588, 0x09cf4f3c};
constexpr std::array<u32, 4> iv = {0x00010203, 0x04050607, 0x08090a0b, 0x0c0d0e0f};
constexpr std::array<u32, 4> plaintext = {0x6bc1bee2, 0x2e409f96, 0xe93d7e11, 0x7393172a};
constexpr std::array<u32, 4> ciphertext = {0x7649abac, 0x8119b246, 0xcee98e9b, 0x12e9197d};
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
for (const bool split : {false, true})
{
for (const u32 offset : {0xf080u, 0x10020u, 0x17ff0u})
{
for (const bool decrypt : {false, true})
{
SCOPED_TRACE(::testing::Message()
<< "split=" << split << " offset=" << offset << " decrypt=" << decrypt);
system.GetWiiIPC().Reset();
StarletMemory memory(system);
memory.Reset();
memory.Write32(0x0d800060, split ? 0x20 : 0);
// Use the non-ROM high aperture for both SRAM A and B.
const u32 cpu_address = StarletMemory::SRAM_MIRROR_BASE + (offset ^ (split ? 0x10000 : 0));
const u32 dma_address = StarletMemory::SRAM_BASE + offset;
for (u32 i = 0; i < 4; ++i)
{
memory.Write32(cpu_address + i * 4, (decrypt ? ciphertext : plaintext)[i]);
memory.Write32(0x0d02000c, key[i]);
memory.Write32(0x0d020010, iv[i]);
}
memory.Write32(0x0d020004, dma_address);
memory.Write32(0x0d020008, dma_address);
const u32 command = 0xd0000000 | (decrypt ? 0x08000000 : 0);
memory.Write32(0x0d020000, command);
for (u32 i = 0; i < 4; ++i)
EXPECT_EQ(memory.Read32(cpu_address + i * 4), (decrypt ? plaintext : ciphertext)[i]);
EXPECT_EQ(memory.Read32(0x0d020004), dma_address + 16);
EXPECT_EQ(memory.Read32(0x0d020008), dma_address + 16);
EXPECT_EQ(memory.Read32(0x0d020000), command & ~0x80000000u);
EXPECT_NE(memory.Read32(0x0d800038) & (1U << 2), 0u);
}
}
}
}
TEST(StarletDMA, CopyCrossesPhysicalBankBoundaryButDoesNotWrapPastSRAM)
{
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
for (const bool split : {false, true})
{
system.GetWiiIPC().Reset();
StarletMemory memory(system);
memory.Reset();
memory.Write32(0x0d800060, split ? 0x20 : 0);
const auto cpu_address = [split](u32 offset) {
return StarletMemory::SRAM_MIRROR_BASE + (offset ^ (split ? 0x10000 : 0));
};
for (u32 i = 0; i < StarletMemory::SRAM_SIZE; ++i)
memory.Write8(cpu_address(i), 0x5a);
for (u32 i = 0; i < 32; ++i)
memory.Write8(cpu_address(0xfff0 + i), static_cast<u8>(i + 1));
// Copy crosses A -> B on input, then B -> unmapped space on output.
memory.Write32(0x0d020004, 0x0d40fff0);
memory.Write32(0x0d020008, 0x0d417ff0);
memory.Write32(0x0d020000, 0x80000001); // Two blocks, AES disabled.
for (u32 i = 0; i < StarletMemory::SRAM_SIZE; ++i)
{
const u8 expected = i >= 0x17ff0 ? static_cast<u8>(i - 0x17ff0 + 1) :
i >= 0xfff0 && i < 0x10010 ? static_cast<u8>(i - 0xfff0 + 1) :
0x5a;
ASSERT_EQ(memory.Read8(cpu_address(i)), expected) << "offset=" << i << " split=" << split;
}
memory.Write32(0x0d020004, 0x0d417ff0);
memory.Write32(0x0d020008, 0x0d400020);
memory.Write32(0x0d020000, 0x80000001);
for (u32 i = 0; i < 32; ++i)
EXPECT_EQ(memory.Read8(cpu_address(0x20 + i)), i < 16 ? i + 1 : 0u);
// The CPU still sees its own split hole, not the DMA mapping.
EXPECT_EQ(memory.Read32(StarletMemory::SRAM_MIRROR_BASE + (split ? 0x8000 : 0x18000)), 0u);
}
}
TEST(StarletDMA, SHAUsesPhysicalSRAMInSplitMode)
{
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
system.GetWiiIPC().Reset();
StarletMemory memory(system);
memory.Reset();
memory.Write32(0x0d800060, 0x20);
// One padded SHA-1 block for "abc" in SRAM A.
memory.Write32(0xfffff080, 0x61626380);
memory.Write32(0xfffff0bc, 24);
constexpr std::array<u32, 5> initial = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476,
0xc3d2e1f0};
constexpr std::array<u32, 5> digest = {0xa9993e36, 0x4706816a, 0xba3e2571, 0x7850c26c,
0x9cd0d89d};
for (u32 i = 0; i < 5; ++i)
memory.Write32(0x0d030008 + i * 4, initial[i]);
memory.Write32(0x0d030004, 0x0d40f080);
memory.Write32(0x0d030000, 0x80000000);
for (u32 i = 0; i < 5; ++i)
EXPECT_EQ(memory.Read32(0x0d030008 + i * 4), digest[i]);
EXPECT_EQ(memory.Read32(0x0d030004), 0x0d40f0c0u);
}
TEST(StarletDMA, OHCIAndNANDUsePhysicalSRAMWithoutChangingCPUView)
{
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
system.GetWiiIPC().Reset();
StarletMemory memory(system);
memory.Reset();
memory.Write32(0x0d800060, 0x20);
// OHCI writes its little-endian frame number into HCCA via DMA.
memory.Write32(0x0d050018, 0x0d40f000);
memory.Write32(0x0d050004, 2U << 6);
memory.AdvanceCycles(243000);
EXPECT_EQ(memory.Read16(0xfffff080), 0x0100u);
EXPECT_EQ(memory.Read16(0x0d40f080), 0u); // CPU split hole remains a hole.
// NAND ID needs no dump and exercises the same bus-master destination mapping.
memory.Write32(0x0d010010, 0x0d40f080);
memory.Write32(0x0d010000, 0x80902005); // EXEC, READ_ID, READ, five bytes.
constexpr std::array<u8, 5> id = {0xec, 0xdc, 0x10, 0x95, 0x54};
for (u32 i = 0; i < id.size(); ++i)
EXPECT_EQ(memory.Read8(0xfffff080 + i), id[i]);
}
TEST(StarletRegisters, CachePreservesSparseValuesAndCollisions)
{
constexpr u32 address_a = 0x0d900100;
@@ -429,6 +562,54 @@ TEST(StarletRegisters, WideTimerAndInterruptAccessesMatchHardwareSemantics)
EXPECT_EQ(memory.Read32(arm_irq_mask), 0x800619efu);
}
TEST(StarletInterrupts, DriveAndResetUseDistinctHollywoodLines)
{
// Hollywood IRQ numbers, not the Broadway Processor Interface's DVD line.
EXPECT_EQ(static_cast<u32>(INT_CAUSE_DI), 1u << 18);
EXPECT_EQ(static_cast<u32>(INT_CAUSE_RST_BUTTON), 1u << 17);
EXPECT_EQ(INT_CAUSE_DI & INT_CAUSE_RST_BUTTON, 0u);
}
TEST(StarletInterrupts, DriveInterruptReachesNativeIOSMask)
{
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
auto& ipc = system.GetWiiIPC();
ipc.Reset();
StarletMemory memory(system);
memory.Reset();
constexpr u32 irq_flags = 0x0d800038;
constexpr u32 irq_mask = 0x0d80003c;
// The real IOS56 mask captured during the Shop Channel failure enables IRQ18,
// but deliberately does not enable IRQ9, where DI was previously misrouted.
constexpr u32 ios_mask = 0x800619ef;
constexpr u32 drive_irq = 1u << 18;
memory.Write32(irq_mask, ios_mask);
EXPECT_FALSE(ipc.IsStarletIRQAsserted());
for (unsigned repeat = 0; repeat < 2; ++repeat)
{
// This is the same cause and interrupt entry point used by DVDInterface.
ipc.SetStarletInterrupt(INT_CAUSE_DI, true);
EXPECT_EQ(memory.Read32(irq_flags), drive_irq);
EXPECT_TRUE(ipc.IsStarletIRQAsserted());
memory.Write32(irq_mask, ios_mask & ~drive_irq);
EXPECT_FALSE(ipc.IsStarletIRQAsserted());
memory.Write32(irq_mask, ios_mask);
EXPECT_TRUE(ipc.IsStarletIRQAsserted());
memory.Write32(irq_flags, drive_irq);
EXPECT_EQ(memory.Read32(irq_flags), 0u);
EXPECT_FALSE(ipc.IsStarletIRQAsserted());
ipc.SetStarletInterrupt(INT_CAUSE_DI, false);
}
ipc.SetStarletInterrupt(INT_CAUSE_DI, true);
ipc.SetStarletInterrupt(INT_CAUSE_DI, false);
EXPECT_EQ(memory.Read32(irq_flags), 0u);
EXPECT_FALSE(ipc.IsStarletIRQAsserted());
ipc.Reset();
}
TEST(StarletTimer, RunsAtOneTickPer128ARMCycles)
{
constexpr u32 timer = 0x0d800010;
@@ -2343,6 +2524,169 @@ TEST(StarletARMCore, ARMJitMatchesARM926UnalignedWordTransfersWithoutFallback)
#endif
}
TEST(StarletARMCore, ThumbJitKeepsAlignedBusMemoryInsideNativeBlock)
{
#if defined(_M_X86_64)
for (const bool big_endian : {false, true})
{
SCOPED_TRACE(big_endian);
TestBus interpreter_bus;
TestBus jit_bus;
ARMCore interpreter(interpreter_bus);
ARMCore jit(jit_bus);
const auto setup = [big_endian](TestBus& bus, ARMCore& core) {
bus.SetFastmemEnabled(true);
// Mixing r0/r1 cached registers, signed reads, and bus writes must retain flags and order.
bus.WriteThumb(0x00, 0x6014); // str r4, [r2]
bus.WriteThumb(0x02, 0x6810); // ldr r0, [r2]
bus.WriteThumb(0x04, 0x7054); // strb r4, [r2, #1]
bus.WriteThumb(0x06, 0x56d1); // ldrsb r1, [r2, r3]
bus.WriteThumb(0x08, 0x8054); // strh r4, [r2, #2]
bus.WriteThumb(0x0a, 0x8855); // ldrh r5, [r2, #2]
bus.WriteThumb(0x0c, 0x5ed6); // ldrsh r6, [r2, r3]
bus.WriteThumb(0x0e, 0x4050); // eor r0, r2
bus.WriteThumb(0x10, 0x9000); // str r0, [sp]
bus.WriteThumb(0x12, 0x9900); // ldr r1, [sp]
bus.WriteThumb(0x14, 0xe7fe); // b .
if (!big_endian)
{
for (u32 offset = 0; offset <= 0x14; offset += 2)
bus.WriteThumb(offset, static_cast<u16>((bus[offset + 1] << 8) | bus[offset]));
}
core.SetBigEndian(big_endian);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor) | ARMCore::CPSR_T);
core.SetRegister(2, 0x0d800000);
core.SetRegister(3, 0);
core.SetRegister(4, 0x80fe91f3);
core.SetRegister(13, 0x0d800000);
};
setup(interpreter_bus, interpreter);
setup(jit_bus, jit);
jit.SetJitEnabled(true);
EXPECT_EQ(interpreter.RunCycles(11), 11u);
EXPECT_EQ(jit.RunCycles(11), 11u);
for (u32 reg = 0; reg < 16; ++reg)
EXPECT_EQ(jit.GetRegister(reg), interpreter.GetRegister(reg)) << "r" << reg;
EXPECT_EQ(jit.GetCPSR(), interpreter.GetCPSR());
EXPECT_EQ(jit_bus.GetMMIOWord(), interpreter_bus.GetMMIOWord());
EXPECT_EQ(jit.GetJitFallbackInstructionCount(), 0u);
EXPECT_EQ(jit.GetJitNativeExecutedInstructions(), 11u);
}
#endif
}
TEST(StarletARMCore, DISABLED_ThumbBusThroughputBenchmark)
{
#if defined(_M_X86_64)
TestBus bus;
ARMCore jit(bus);
bus.SetFastmemEnabled(true);
// Exact bus reads resemble the IOS loop sampled in the Shop Channel.
bus.WriteThumb(0x00, 0x6810); // ldr r0, [r2]
bus.WriteThumb(0x02, 0x6851); // ldr r1, [r2, #4]
bus.WriteThumb(0x04, 0x6011); // str r1, [r2]
bus.WriteThumb(0x06, 0xe7fb); // b 0
bus.SetSRAMFastmemEnabled(true);
bus.SetBoot0Mapped(false);
bus.SetSRAMSplitMode(true);
bus.WriteSRAM32(0xf000, 0x12345678);
bus.WriteSRAM32(0xf004, 0x87654321);
jit.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor) | ARMCore::CPSR_T);
jit.SetRegister(2, 0xfffff000);
jit.SetJitEnabled(true);
constexpr u64 cycles = 4'000'000;
const auto start = std::chrono::steady_clock::now();
EXPECT_EQ(jit.RunCycles(cycles), cycles);
const auto us = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - start)
.count();
EXPECT_EQ(jit.GetRegister(0), 0x87654321u);
EXPECT_EQ(jit.GetRegister(1), 0x87654321u);
std::cout << "Thumb exact-bus loop: " << us << " us for " << cycles
<< " instructions; fallbacks=" << jit.GetJitFallbackInstructionCount() << '\n';
#endif
}
TEST(StarletARMCore, ThumbJitPreservesUnalignedMemorySemantics)
{
#if defined(_M_X86_64)
for (const bool big_endian : {false, true})
{
SCOPED_TRACE(big_endian);
TestBus interpreter_bus(0x2000);
TestBus jit_bus(0x2000);
ARMCore interpreter(interpreter_bus);
ARMCore jit(jit_bus);
const auto setup = [big_endian](TestBus& bus, ARMCore& core) {
bus.SetFastmemEnabled(true);
const std::array<u16, 7> code{0x58d0, 0x6811, 0x5ed4, 0x50d5, 0x52d6, 0x6017, 0xe7fe};
// Register-offset LDR rotates an aligned word; immediate LDR and odd halfwords retain
// the interpreter's bytewise path, including accesses crossing the 1 KiB TLB boundary.
for (u32 i = 0; i < code.size(); ++i)
{
const u16 op = code[i];
bus.WriteThumb(0x1000 + i * 2, big_endian ? op : static_cast<u16>((op >> 8) | (op << 8)));
}
bus.WriteARM(0x3fc, 0x81fe9273);
bus.WriteARM(0x400, 0xa5b6c7d8);
core.Reset(0x1000);
core.SetBigEndian(big_endian);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor) | ARMCore::CPSR_T);
core.SetRegister(2, 0x3ff);
core.SetRegister(3, 0);
core.SetRegister(5, 0x12345678);
core.SetRegister(6, 0x89ab);
core.SetRegister(7, 0xc0ffee01);
};
setup(interpreter_bus, interpreter);
setup(jit_bus, jit);
jit.SetJitEnabled(true);
EXPECT_EQ(interpreter.RunCycles(7), 7u);
EXPECT_EQ(jit.RunCycles(7), 7u);
for (u32 reg = 0; reg < 16; ++reg)
EXPECT_EQ(jit.GetRegister(reg), interpreter.GetRegister(reg)) << "r" << reg;
EXPECT_EQ(jit.GetCPSR(), interpreter.GetCPSR());
for (u32 address = 0x3fc; address < 0x408; ++address)
EXPECT_EQ(jit_bus[address], interpreter_bus[address]);
EXPECT_EQ(jit.GetJitFallbackInstructionCount(), 6u);
}
#endif
}
TEST(StarletARMCore, ThumbJitBusMemoryPreservesBoot0AndSRAMHoles)
{
#if defined(_M_X86_64)
for (const bool split : {false, true})
{
for (const u32 address : {split ? 0xfffe0000u : 0xffff0000u, split ? 0xfffe9000u : 0xffff9000u})
{
SCOPED_TRACE(address);
TestBus bus;
ARMCore core(bus);
bus.SetFastmemEnabled(true);
bus.SetSRAMFastmemEnabled(true);
bus.SetBoot0Mapped(true);
bus.SetSRAMSplitMode(split);
bus.WriteThumb(0, 0x6014); // str r4, [r2]
bus.WriteThumb(2, 0x6810); // ldr r0, [r2]
bus.WriteThumb(4, 0xe7fe);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor) | ARMCore::CPSR_T);
core.SetRegister(2, address);
core.SetRegister(4, 0x12345678);
core.SetJitEnabled(true);
ASSERT_EQ(core.RunCycles(3), 3u);
EXPECT_EQ(core.GetRegister(0), 0u);
EXPECT_EQ(core.GetJitFallbackInstructionCount(), 0u);
EXPECT_EQ(core.GetJitSlowSRAMAccessCount(), 2u);
EXPECT_TRUE(bus.SRAMCanariesIntact());
// TestBus initializes both SRAM banks and guards with the same canary byte.
for (u32 offset = 0; offset < 0x18000; offset += 4)
EXPECT_EQ(bus.ReadSRAM32(offset), 0xa5a5a5a5u);
}
}
#endif
}
TEST(StarletARMCore, ARMJitUsesReadFastmemForProfiledSplitSRAMMirrorPages)
{
#if defined(_M_X86_64)
@@ -0,0 +1,600 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <vector>
#include <gtest/gtest.h>
#ifdef _WIN32
#include <winsock2.h>
#else
#include <arpa/inet.h>
#include <poll.h>
#endif
#define LIBSLIRP_STATIC
#include "libslirp.h"
#include "Common/ChunkFile.h"
#include "Common/FileUtil.h"
#include "Common/Network.h"
#include "Common/ScopeGuard.h"
#include "Core/Core.h"
#include "Core/IOS/Starlet/AX88772.h"
#include "Core/IOS/Starlet/SlirpNetwork.h"
#include "Core/IOS/Starlet/StarletMemory.h"
#include "Core/System.h"
using IOS::LLE::AX88772;
using Result = AX88772::Result;
namespace
{
std::array<u8, 8> USBSetup(u8 type, u8 request, u16 value, u16 index, u16 length)
{
return {type,
request,
static_cast<u8>(value),
static_cast<u8>(value >> 8),
static_cast<u8>(index),
static_cast<u8>(index >> 8),
static_cast<u8>(length),
static_cast<u8>(length >> 8)};
}
std::vector<u8> Framed(std::span<const u8> frame)
{
const u16 length = static_cast<u16>(frame.size());
std::vector<u8> result{static_cast<u8>(length), static_cast<u8>(length >> 8),
static_cast<u8>(~length), static_cast<u8>(~length >> 8)};
result.insert(result.end(), frame.begin(), frame.end());
return result;
}
std::vector<u8> Control(AX88772& device, u8 type, u8 request, u16 value, u16 index,
std::vector<u8> data = {})
{
auto setup = USBSetup(type, request, value, index, static_cast<u16>(data.size()));
size_t actual = 0;
EXPECT_EQ(device.Transfer(0, 0, setup, &actual), Result::Completed);
EXPECT_EQ(actual, 8u);
if (!data.empty())
{
EXPECT_EQ(device.Transfer(0, (type & 0x80) ? 2 : 1, data, &actual), Result::Completed);
data.resize(actual);
}
EXPECT_EQ(device.Transfer(0, (type & 0x80) ? 1 : 2, {}, &actual), Result::Completed);
return data;
}
void Configure(AX88772& device)
{
Control(device, 0, 9, 1, 0);
Control(device, 0x40, 0x10, 0x88, 0);
Control(device, 0x40, 0x1b, 0x306, 0);
device.SetLink(true);
}
std::string RuntimeDirectory()
{
return File::GetExeDirectory() + "/../Network";
}
} // namespace
TEST(StarletAX88772, EnumeratesSupportedVIDPIDAndFullSpeedEndpoints)
{
AX88772 device({});
const auto descriptor = Control(device, 0x80, 6, 0x100, 0, std::vector<u8>(64));
ASSERT_EQ(descriptor.size(), 18u);
EXPECT_EQ(descriptor[8], 0x95);
EXPECT_EQ(descriptor[9], 0x0b);
EXPECT_EQ(descriptor[10], 0x20);
EXPECT_EQ(descriptor[11], 0x77);
const auto config = Control(device, 0x80, 6, 0x200, 0, std::vector<u8>(255));
ASSERT_EQ(config.size(), 39u);
EXPECT_EQ(config[20], 0x81);
EXPECT_EQ(config[27], 0x82);
EXPECT_EQ(config[29], 64);
EXPECT_EQ(config[34], 3);
}
TEST(StarletAX88772, AddressChangesOnlyAfterStatusStage)
{
AX88772 device({});
size_t actual;
auto setup = USBSetup(0, 5, 7, 0, 0);
ASSERT_EQ(device.Transfer(0, 0, setup, &actual), Result::Completed);
EXPECT_EQ(device.GetAddress(), 0);
ASSERT_EQ(device.Transfer(0, 2, {}, &actual), Result::Completed);
EXPECT_EQ(device.GetAddress(), 7);
device.Reset();
EXPECT_EQ(device.GetAddress(), 0);
}
TEST(StarletAX88772, SplitControlReadsAdvanceRatherThanRepeat)
{
AX88772 device({});
size_t actual;
auto setup = USBSetup(0x80, 6, 0x100, 0, 18);
ASSERT_EQ(device.Transfer(0, 0, setup, &actual), Result::Completed);
std::array<u8, 8> first{};
std::array<u8, 10> second{};
ASSERT_EQ(device.Transfer(0, 2, first, &actual), Result::Completed);
EXPECT_EQ(actual, 8u);
ASSERT_EQ(device.Transfer(0, 2, second, &actual), Result::Completed);
EXPECT_EQ(actual, 10u);
EXPECT_EQ(second[0], 0x95);
}
TEST(StarletAX88772, UnknownAndMalformedRequestsStall)
{
AX88772 device({});
size_t actual;
for (auto setup : {USBSetup(0xc0, 0xff, 0, 0, 2), USBSetup(0x40, 8, 0x10, 0, 3),
USBSetup(0x40, 0x14, 0, 0, 7), USBSetup(0x80, 6, 0x3ff, 0, 10)})
EXPECT_EQ(device.Transfer(0, 0, setup, &actual), Result::Stalled);
}
TEST(StarletAX88772, MACAndMIIRegistersRoundTrip)
{
AX88772 device({});
const std::vector<u8> mac{2, 3, 4, 5, 6, 7};
Control(device, 0x40, 0x14, 0, 0, mac);
EXPECT_EQ(Control(device, 0xc0, 0x13, 0, 0, std::vector<u8>(6)), mac);
Control(device, 0x40, 0x08, 0x10, 4, {0xe1, 1});
EXPECT_EQ(Control(device, 0xc0, 0x07, 0x10, 4, std::vector<u8>(2)), (std::vector<u8>{0xe1, 1}));
device.SetLink(true);
const auto status = Control(device, 0xc0, 0x07, 0x10, 1, std::vector<u8>(2));
EXPECT_EQ(status[0] & 0x24, 0x24);
}
TEST(StarletAX88772, LinkChangesRefreshStatusWithoutWaitingForPeriodicReport)
{
AX88772 device({});
Configure(device);
std::array<u8, 8> event{};
size_t actual;
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed);
EXPECT_EQ(event[2], 1);
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending);
device.SetLink(false);
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed);
EXPECT_EQ(event[2], 0);
}
TEST(StarletAX88772, UnchangedLinkGetsPeriodicStatusWithoutBusyPollingOrBacklog)
{
AX88772 device({});
Configure(device);
const auto config = Control(device, 0x80, 6, 0x200, 0, std::vector<u8>(255));
ASSERT_EQ(config.size(), 39u);
const unsigned interval = config[24];
ASSERT_EQ(interval, 10u);
std::array<u8, 8> event{};
size_t actual = 0;
for (unsigned report = 0; report < 3; ++report)
{
ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed);
EXPECT_EQ(actual, 8u);
EXPECT_EQ(event[2], 1);
for (unsigned frame = 0; frame < interval; ++frame)
{
for (unsigned poll = 0; poll < 3; ++poll)
{
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending);
EXPECT_EQ(actual, 0u);
}
device.AdvanceUSBFrame();
}
}
// Leaving the endpoint unpolled coalesces reports; it does not queue them.
for (unsigned frame = 0; frame < 1000; ++frame)
device.AdvanceUSBFrame();
ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed);
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending);
}
TEST(StarletAX88772, PeriodicStatusWaitsForHardwareMDIOOwnership)
{
AX88772 device({});
Configure(device);
std::array<u8, 8> event{};
size_t actual = 0;
ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed);
Control(device, 0x40, 0x06, 0, 0);
for (unsigned frame = 0; frame < 20; ++frame)
device.AdvanceUSBFrame();
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending);
Control(device, 0x40, 0x0a, 0, 0);
ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed);
EXPECT_EQ(event, (std::array<u8, 8>{0xa1, 0, 1, 0, 0x2d, 0x78, 0xe1, 0x45}));
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending);
}
TEST(StarletAX88772, StateRoundTripPreservesPeriodicStatusPhaseAndResetClearsIt)
{
AX88772 device({});
Configure(device);
std::array<u8, 8> event{};
size_t actual = 0;
ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed);
for (unsigned frame = 0; frame < 4; ++frame)
device.AdvanceUSBFrame();
std::vector<u8> state(65536);
u8* ptr = state.data();
PointerWrap writer(&ptr, state.size(), PointerWrap::Mode::Write);
device.DoState(writer);
ASSERT_TRUE(writer.IsWriteMode());
const size_t size = ptr - state.data();
AX88772 restored({});
ptr = state.data();
PointerWrap reader(&ptr, size, PointerWrap::Mode::Read);
restored.DoState(reader);
ASSERT_TRUE(reader.IsReadMode());
for (unsigned frame = 0; frame < 6; ++frame)
{
EXPECT_EQ(restored.Transfer(1, 2, event, &actual), Result::Pending);
restored.AdvanceUSBFrame();
}
ASSERT_EQ(restored.Transfer(1, 2, event, &actual), Result::Completed);
EXPECT_EQ(event[2], 1);
restored.Reset();
EXPECT_EQ(restored.Transfer(1, 2, event, &actual), Result::Stalled);
Configure(restored);
EXPECT_EQ(restored.Transfer(1, 2, event, &actual), Result::Completed);
}
TEST(StarletAX88772, EEPROMAndPHYAddressRegisterDescribeTheSameHardware)
{
AX88772 device({});
const auto eeprom = Control(device, 0xc0, 0x0b, 0x11, 0, std::vector<u8>(2));
EXPECT_EQ(eeprom, (std::vector<u8>{0x10, 0xe0}));
EXPECT_EQ(Control(device, 0xc0, 0x19, 0, 0, std::vector<u8>(2)), (std::vector<u8>{0xe0, 0x10}));
// Accessing an absent PHY must not poison the USB control endpoint.
Control(device, 0x40, 0x08, 0xff, 4, {0, 0});
EXPECT_EQ(Control(device, 0xc0, 0x07, 0xff, 4, std::vector<u8>(2)),
(std::vector<u8>{0xff, 0xff}));
// Only the low five bits reach MDIO, as specified by the device datasheet.
Control(device, 0x40, 0x08, 0xf0, 0x24, {0x23, 1});
EXPECT_EQ(Control(device, 0xc0, 0x07, 0x10, 4, std::vector<u8>(2)), (std::vector<u8>{0x23, 1}));
}
TEST(StarletAX88772, InterruptIncludesConfiguredPHYRegistersAndWaitsForMDIOOwnership)
{
AX88772 device({});
Configure(device);
EXPECT_EQ(Control(device, 0xc0, 0x0b, 0x0f, 0, std::vector<u8>(2)), (std::vector<u8>{5, 1}));
EXPECT_EQ(Control(device, 0xc0, 0x0b, 0x10, 0, std::vector<u8>(2)), (std::vector<u8>{0xee, 5}));
Control(device, 0x40, 0x06, 0, 0);
std::array<u8, 8> event{};
size_t actual;
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending);
Control(device, 0x40, 0x0a, 0, 0);
ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed);
EXPECT_EQ(event, (std::array<u8, 8>{0xa1, 0, 1, 0, 0x2d, 0x78, 0xe1, 0x45}));
EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending);
}
TEST(StarletAX88772, TransmitsOddLengthFramesAcrossUSBTransfers)
{
std::vector<std::vector<u8>> received;
AX88772 device(
[&](std::span<const u8> frame) { received.emplace_back(frame.begin(), frame.end()); });
Configure(device);
std::vector<u8> frame(61, 0x42);
auto packet = Framed(frame);
size_t actual;
EXPECT_EQ(device.Transfer(3, 1, std::span(packet).first(17), &actual), Result::Completed);
EXPECT_TRUE(received.empty());
EXPECT_EQ(device.Transfer(3, 1, std::span(packet).subspan(17), &actual), Result::Completed);
ASSERT_EQ(received.size(), 1u);
EXPECT_EQ(received[0], frame);
packet[2] ^= 1;
EXPECT_EQ(device.Transfer(3, 1, packet, &actual), Result::Stalled);
EXPECT_EQ(received.size(), 1u);
}
TEST(StarletAX88772, ReceiveFramingAndResetPreservePacketBoundaries)
{
AX88772 device({});
Configure(device);
std::vector<u8> frame(61, 0x42);
std::fill_n(frame.begin(), 6, 0xff);
device.ReceiveFrame(frame);
std::array<u8, 2048> buffer{};
size_t actual;
ASSERT_EQ(device.Transfer(2, 2, buffer, &actual), Result::Completed);
EXPECT_EQ(actual, 66u);
EXPECT_EQ(buffer[0], 61);
EXPECT_EQ(buffer[2], static_cast<u8>(~61));
EXPECT_TRUE(std::equal(frame.begin(), frame.end(), buffer.begin() + 4));
EXPECT_EQ(device.Transfer(2, 2, buffer, &actual), Result::Pending);
device.ReceiveFrame(frame);
device.Reset();
EXPECT_EQ(device.Transfer(2, 2, buffer, &actual), Result::Stalled);
Configure(device);
EXPECT_EQ(device.Transfer(2, 2, buffer, &actual), Result::Pending);
}
TEST(StarletEthernetNAT, PollEventMappingPreservesReadWritePriorityAndErrors)
{
using namespace IOS::LLE::SlirpSocketPolling;
for (int flags = 0; flags < 8; ++flags)
EXPECT_EQ(ToSlirpEvents(ToNativeEvents(flags)), flags);
EXPECT_EQ(ToSlirpEvents(POLLERR | POLLHUP), SLIRP_POLL_ERR | SLIRP_POLL_HUP);
EXPECT_EQ(ToSlirpEvents(POLLNVAL), SLIRP_POLL_ERR);
#ifdef _WIN32
EXPECT_EQ(ToNativeEvents(SLIRP_POLL_IN), POLLRDNORM);
EXPECT_EQ(ToNativeEvents(SLIRP_POLL_PRI), POLLRDBAND);
EXPECT_EQ(ToNativeEvents(SLIRP_POLL_IN | SLIRP_POLL_PRI) & POLLPRI, 0);
#endif
}
#ifdef _WIN32
TEST(StarletEthernetNAT, WinsockTCPReadAndPeerCloseWorkWithSlirpRequestedEvents)
{
using namespace IOS::LLE::SlirpSocketPolling;
WSADATA wsadata{};
ASSERT_EQ(WSAStartup(MAKEWORD(2, 2), &wsadata), 0);
Common::ScopeGuard cleanup([] { WSACleanup(); });
const SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
ASSERT_NE(listener, INVALID_SOCKET);
Common::ScopeGuard close_listener([&] { closesocket(listener); });
sockaddr_in address{};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
ASSERT_EQ(bind(listener, reinterpret_cast<const sockaddr*>(&address), sizeof(address)), 0);
ASSERT_EQ(listen(listener, 1), 0);
int address_size = sizeof(address);
ASSERT_EQ(getsockname(listener, reinterpret_cast<sockaddr*>(&address), &address_size), 0);
const SOCKET client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
ASSERT_NE(client, INVALID_SOCKET);
Common::ScopeGuard close_client([&] { closesocket(client); });
ASSERT_EQ(connect(client, reinterpret_cast<const sockaddr*>(&address), sizeof(address)), 0);
u_long nonblocking = 1;
ASSERT_EQ(ioctlsocket(client, FIONBIO, &nonblocking), 0);
const SOCKET server = accept(listener, nullptr, nullptr);
ASSERT_NE(server, INVALID_SOCKET);
Common::ScopeGuard close_server([&] { closesocket(server); });
ASSERT_EQ(send(server, "test", 4, 0), 4);
ASSERT_EQ(shutdown(server, SD_SEND), 0);
// libslirp adds PRI immediately after a TCP connection is established. An
// unsupported Winsock event here used to prevent receiving any data or FIN.
WSAPOLLFD fd{client, ToNativeEvents(SLIRP_POLL_IN | SLIRP_POLL_PRI), 0};
ASSERT_EQ(WSAPoll(&fd, 1, 1000), 1) << WSAGetLastError();
EXPECT_NE(ToSlirpEvents(fd.revents) & SLIRP_POLL_IN, 0);
std::array<char, 4> data{};
ASSERT_EQ(recv(client, data.data(), static_cast<int>(data.size()), 0), 4);
EXPECT_EQ(data, (std::array<char, 4>{'t', 'e', 's', 't'}));
fd.revents = 0;
ASSERT_EQ(WSAPoll(&fd, 1, 1000), 1) << WSAGetLastError();
EXPECT_NE(ToSlirpEvents(fd.revents) & (SLIRP_POLL_IN | SLIRP_POLL_HUP), 0);
EXPECT_EQ(recv(client, data.data(), static_cast<int>(data.size()), 0), 0);
}
#endif
TEST(StarletEthernetNAT, MissingRuntimeFailsExplicitly)
{
IOS::LLE::SlirpNetwork network;
std::string error;
EXPECT_FALSE(network.Start("/nonexistent-dolphin-slirp-runtime", {}, &error));
EXPECT_FALSE(error.empty());
}
TEST(StarletEthernetNAT, LibslirpAnswersARPThroughAX88772BulkEndpoints)
{
if (!File::IsDirectory(RuntimeDirectory()))
GTEST_SKIP() << "Optional local libslirp runtime absent";
IOS::LLE::SlirpNetwork network;
AX88772 device([&](std::span<const u8> frame) { network.Input(frame); });
std::string error;
ASSERT_TRUE(network.Start(
RuntimeDirectory(), [&](std::span<const u8> frame) { device.ReceiveFrame(frame); }, &error))
<< error;
Configure(device);
Common::MACAddress mac{2, 0x44, 0x4f, 0x4c, 0, 1};
Common::ARPPacket request(Common::MACAddress{255, 255, 255, 255, 255, 255}, mac);
request.arp_header.opcode = htons(1);
request.arp_header.sender_address = mac;
request.arp_header.sender_ip = htonl(0x0a00020f);
request.arp_header.target_ip = htonl(0x0a000202);
auto framed = Framed(request.Build());
size_t actual;
ASSERT_EQ(device.Transfer(3, 1, framed, &actual), Result::Completed);
network.Poll(243000);
std::array<u8, 2048> buffer{};
ASSERT_EQ(device.Transfer(2, 2, buffer, &actual), Result::Completed);
ASSERT_GE(actual, 46u);
const auto arp =
Common::PacketView(buffer.data() + 4, buffer[0] | (buffer[1] << 8)).GetARPPacket();
ASSERT_TRUE(arp.has_value());
EXPECT_EQ(ntohs(arp->arp_header.opcode), 2);
EXPECT_EQ(arp->arp_header.sender_ip, htonl(0x0a000202));
}
TEST(StarletEthernetNAT, LibslirpDHCPProvidesAddressRouterAndDNS)
{
if (!File::IsDirectory(RuntimeDirectory()))
GTEST_SKIP() << "Optional local libslirp runtime absent";
IOS::LLE::SlirpNetwork network;
std::vector<std::vector<u8>> replies;
std::string error;
ASSERT_TRUE(network.Start(
RuntimeDirectory(),
[&](std::span<const u8> frame) { replies.emplace_back(frame.begin(), frame.end()); }, &error))
<< error;
Common::MACAddress mac{2, 0x44, 0x4f, 0x4c, 0, 1};
Common::DHCPPacket discover;
discover.body.message_type = 1;
discover.body.hardware_type = 1;
discover.body.hardware_addr = 6;
discover.body.transaction_id = htonl(0x12345678);
discover.body.boot_flag = htons(0x8000);
discover.body.client_mac = mac;
discover.AddOption(53, {1});
discover.AddOption(55, {1, 3, 6});
sockaddr_in from{}, to{};
from.sin_port = htons(68);
to.sin_addr.s_addr = 0xffffffff;
to.sin_port = htons(67);
Common::UDPPacket packet(Common::MACAddress{255, 255, 255, 255, 255, 255}, mac, from, to,
discover.Build());
network.Input(packet.Build());
network.Poll(243000);
ASSERT_FALSE(replies.empty());
const auto udp = Common::PacketView(replies.back().data(), replies.back().size()).GetUDPPacket();
ASSERT_TRUE(udp.has_value());
ASSERT_GE(udp->data.size(), Common::DHCPBody::SIZE);
Common::DHCPPacket offer(udp->data);
EXPECT_EQ(offer.body.transaction_id, discover.body.transaction_id);
EXPECT_EQ(offer.body.your_ip, htonl(0x0a00020f));
EXPECT_TRUE(std::ranges::any_of(offer.options, [](const auto& option) {
return option == std::vector<u8>{3, 4, 10, 0, 2, 2};
}));
EXPECT_TRUE(std::ranges::any_of(offer.options, [](const auto& option) {
return option == std::vector<u8>{6, 4, 10, 0, 2, 3};
}));
}
TEST(StarletEthernetNAT, ExternalOHCIEnumeratesDeviceWithoutTouchingBluetooth)
{
if (!File::IsDirectory(RuntimeDirectory()))
GTEST_SKIP() << "Optional local libslirp runtime absent";
Core::DeclareAsCPUThread();
IOS::LLE::StarletMemory memory(Core::System::GetInstance());
memory.Reset();
std::string error;
ASSERT_TRUE(memory.EnableEthernetNAT(RuntimeDirectory(), &error)) << error;
constexpr u32 ohci = 0x0d050000, hcca = 0x0d404000, ed = hcca + 0x100;
constexpr u32 setup_td = hcca + 0x200, data_td = hcca + 0x210, status_td = hcca + 0x220,
tail = hcca + 0x230;
constexpr u32 setup_buffer = hcca + 0x300, data_buffer = hcca + 0x400;
const auto put = [&](u32 addr, u32 value) {
for (u32 i = 0; i < 4; ++i)
memory.Write8(addr + i, static_cast<u8>(value >> (8 * i)));
};
const auto get = [&](u32 addr) {
u32 value = 0;
for (u32 i = 0; i < 4; ++i)
value |= u32(memory.Read8(addr + i)) << (8 * i);
return value;
};
EXPECT_NE(memory.Read32(ohci + 0x54) & 1, 0u);
const u32 bluetooth_before = memory.Read32(0x0d060054);
memory.Write32(ohci + 0x54, 2); // Enable connected port.
const auto setup = USBSetup(0x80, 6, 0x100, 0, 18);
for (u32 i = 0; i < 8; ++i)
memory.Write8(setup_buffer + i, setup[i]);
put(ed, 64u << 16);
put(ed + 4, tail);
put(ed + 8, setup_td);
put(ed + 12, 0);
put(setup_td, 0);
put(setup_td + 4, setup_buffer);
put(setup_td + 8, data_td);
put(setup_td + 12, setup_buffer + 7);
put(data_td, 2u << 19);
put(data_td + 4, data_buffer);
put(data_td + 8, status_td);
put(data_td + 12, data_buffer + 17);
put(status_td, 1u << 19);
put(status_td + 4, 0);
put(status_td + 8, tail);
put(status_td + 12, 0);
memory.Write32(ohci + 0x18, hcca);
memory.Write32(ohci + 0x20, ed);
memory.Write32(ohci + 4, 0x90); // Operational + control list.
EXPECT_EQ(get(ed + 8) & ~0xfu, tail);
EXPECT_EQ(memory.Read8(data_buffer + 8), 0x95);
EXPECT_EQ(memory.Read8(data_buffer + 10), 0x20);
EXPECT_EQ(memory.Read32(0x0d060054), bluetooth_before);
}
TEST(StarletEthernetNAT, ExternalOHCICompletesRepeatedStatusReadsWithoutLinkChanges)
{
if (!File::IsDirectory(RuntimeDirectory()))
GTEST_SKIP() << "Optional local libslirp runtime absent";
Core::DeclareAsCPUThread();
IOS::LLE::StarletMemory memory(Core::System::GetInstance());
memory.Reset();
std::string error;
ASSERT_TRUE(memory.EnableEthernetNAT(RuntimeDirectory(), &error)) << error;
constexpr u32 ohci = 0x0d050000, hcca = 0x0d404000, ed = hcca + 0x100;
constexpr u32 td = hcca + 0x200, status_td = td + 0x10, tail = td + 0x20;
constexpr u32 buffer = hcca + 0x300;
const auto put = [&](u32 addr, u32 value) {
for (u32 i = 0; i < 4; ++i)
memory.Write8(addr + i, static_cast<u8>(value >> (8 * i)));
};
const auto get = [&](u32 addr) {
u32 value = 0;
for (u32 i = 0; i < 4; ++i)
value |= u32(memory.Read8(addr + i)) << (8 * i);
return value;
};
// Configure through endpoint zero, then submit the same synchronous status
// reads used by IOS's Ethernet link polling thread. No guest code is bypassed.
memory.Write32(ohci + 0x54, 2);
const auto setup = USBSetup(0, 9, 1, 0, 0);
for (u32 i = 0; i < 8; ++i)
memory.Write8(buffer + i, setup[i]);
put(ed, 64u << 16);
put(ed + 4, tail);
put(ed + 8, td);
put(td, 0);
put(td + 4, buffer);
put(td + 8, status_td);
put(td + 12, buffer + 7);
put(status_td, 2u << 19);
put(status_td + 8, tail);
memory.Write32(ohci + 0x18, hcca);
memory.Write32(ohci + 0x20, ed);
memory.Write32(ohci + 4, 0x90);
ASSERT_EQ(get(ed + 8) & ~0xfu, tail);
memory.Write32(ohci + 0x0c, 2); // Acknowledge the configuration completion.
for (u32 slot = 0; slot < 32; ++slot)
put(hcca + slot * 4, ed);
put(ed, (8u << 16) | (2u << 11) | (1u << 7));
const auto rearm = [&] {
put(td, 0xf0000000u | (2u << 19));
put(td + 4, buffer);
put(td + 8, tail);
put(td + 12, buffer + 7);
put(ed + 8, td);
};
rearm();
memory.Write32(ohci + 4, 0x84); // Operational + periodic list.
ASSERT_EQ(get(ed + 8) & ~0xfu, tail);
EXPECT_EQ(memory.Read8(buffer + 2), 1);
memory.Write32(ohci + 0x0c, 2);
rearm();
constexpr u64 cycles_per_ms = 243000;
memory.AdvanceCycles(9 * cycles_per_ms);
EXPECT_EQ(get(ed + 8) & ~0xfu, td);
memory.AdvanceCycles(cycles_per_ms);
EXPECT_EQ(get(ed + 8) & ~0xfu, tail);
EXPECT_EQ(get(hcca + 0x84), td);
EXPECT_NE(memory.Read32(ohci + 0x0c) & 2, 0u);
EXPECT_EQ(memory.Read8(buffer + 2), 1);
}
TEST(StarletEthernetNAT, EHCIHandsFullSpeedDeviceBackToExternalOHCI)
{
if (!File::IsDirectory(RuntimeDirectory()))
GTEST_SKIP() << "Optional local libslirp runtime absent";
Core::DeclareAsCPUThread();
IOS::LLE::StarletMemory memory(Core::System::GetInstance());
memory.Reset();
std::string error;
ASSERT_TRUE(memory.EnableEthernetNAT(RuntimeDirectory(), &error)) << error;
constexpr u32 ehci = 0x0d040000, ohci_port = 0x0d050054;
EXPECT_NE(memory.Read32(ohci_port) & 1, 0u);
memory.Write32(ehci + 0x50, 1); // CONFIGFLAG claims the external ports.
EXPECT_EQ(memory.Read32(ohci_port) & 3, 0u);
EXPECT_NE(memory.Read32(ehci + 0x54) & 1, 0u);
memory.Write32(ehci + 0x54, 0x3000); // Port power + companion owner.
EXPECT_NE(memory.Read32(ohci_port) & 1, 0u);
memory.Write32(ehci + 0x54, 0x2000); // Power off.
EXPECT_EQ(memory.Read32(ohci_port) & 3, 0u);
memory.Write32(ehci + 0x54, 0x1100); // Powered reset detects full speed.
EXPECT_NE(memory.Read32(ehci + 0x54) & 0x2000, 0u);
EXPECT_NE(memory.Read32(ohci_port) & 1, 0u);
}
+14 -4
View File
@@ -16,6 +16,9 @@ that boot ROMs, console keys, and NAND contents cannot accidentally be committed
## Configuration
For the opt-in virtual USB Ethernet adapter and user-mode NAT, see
[Wii LLE Ethernet](Wii_LLE_Ethernet.md). It is independent of IOS HLE networking.
Add the following values to Dolphin's main configuration:
```ini
@@ -121,12 +124,19 @@ X2 and immediately submit the next request with X1 before Starlet is scheduled a
- An x86-64 Starlet JIT translates the observed ARM and Thumb integer, branch, interworking and
memory-transfer subset. Its inline generation-tagged TLB and direct fastmem paths cover ordinary
MEM1/MEM2 accesses plus a measured, direction-specific subset of single SRAM reads. Every SRAM
write, register-list transfer, TLB miss, MMIO access, protected boot0 overlay, invalid SRAM
aperture or unsupported instruction remains an architectural side exit: registers are flushed,
the exact interpreter/device operation runs, and the dispatcher re-samples IRQ/FIQ, IPC yield,
CP15 and translation state before another native block executes.
write and MMIO access still uses the exact bus. Aligned Thumb loads/stores can call that bus
directly from native code, including 8-/16-/32-bit and signed reads, without re-decoding the
instruction or returning through the dispatcher. Unaligned Thumb accesses retain interpreter
semantics, including rotated word loads and halfwords crossing a translation boundary.
Protected boot0 overlays and invalid SRAM apertures remain enforced by the bus.
- Big-endian Starlet address space, 96 KiB of physical SRAM (64 KiB bank A plus 32 KiB bank B)
exposed through the hardware's unusual 128 KiB windows, plus shared MEM1/MEM2 access.
Device DMA uses the physical A/B bank layout at `0x0d400000`, independent of the CPU's
`HW_SRNPROT.SM` bank swap. Applying the CPU mapping twice discarded AES output in IOS's SRAM
stack and left `IOSC_GenerateRand` callers retrying during the Shop SSL handshake. The common
DMA accessors cover AES, SHA, NAND, SD/SDIO and OHCI without changing CPU aliases or boot0
protection. Known-answer AES/SHA tests and bank-boundary tests reproduce the old defect.
See MINI's [CPU-to-DMA address conversion](https://github.com/fail0verflow/mini/blob/master/memory.c).
- Raw NAND reads, chip identification/status, Wii ECC generation, ECC-enabled page programming
(including the calculated-ECC DMA side buffer and random spare input), and 64-page block erase.
Programming obeys the NAND 1-to-0 bit rule.
+120
View File
@@ -0,0 +1,120 @@
# Experimental Wii LLE Ethernet + NAT
This opt-in device keeps the original ARM IOS USB, Ethernet and IP drivers in charge.
It presents an AX88772A-family USB Ethernet adapter (VID `0b95`, PID `7720`) on the
external OHCI0 controller. Ethernet frames are passed to libslirp for user-mode NAT.
The internal OHCI1 Bluetooth device is independent. No IOS socket calls are intercepted.
## Windows setup
From the repository directory:
```powershell
.\Setup-Wii-LLE-Network.ps1
.\Run-Wii-IOS-LLE.ps1 -Ethernet
```
Both scripts accept `-BuildDirectory` (default `.starlet_msvc2`). The launcher also
accepts `-UserDirectory`; use an isolated copy of the user profile for initial tests.
Its default remains `.starlet_user3`. Launching without `-Ethernet` leaves this device
disabled. Alternatively set `[Core] WiiLLEEthernet = True` when launching without
this wrapper. It requires Wii IOS LLE and does not alter normal IOS HLE networking.
The setup script downloads version-pinned, SHA-256-checked official MSYS2 UCRT64
packages into the ignored `.starlet_network` cache. DLLs and their licenses are placed
under `Binaries/Network`; it does not install a driver, change PATH, configure a
Windows bridge, or add firewall rules. Runtime loading uses that directory explicitly.
The public libslirp 4.9.3 headers and copyright notice are included under
`Externals/libslirp`; libslirp is loaded dynamically, not linked into Dolphin.
Configure a **wired connection**, with automatic IP address and DNS, in the Wii's
Internet settings. The virtual subnet is `10.0.2.0/24`: gateway `10.0.2.2`, first
DHCP lease `10.0.2.15`, DNS proxy `10.0.2.3`. No inbound port mappings, TFTP directory,
command forwarding or loopback access to host services are enabled. This is NAT,
not a LAN bridge; other LAN devices cannot initiate connections to the guest.
Internet access still depends on the host's connectivity and firewall policy.
Normal network settings are saved through the existing NAND journal. The original
`dumps/nand.bin` is never changed. Avoid accepting a system update during initial
connectivity testing; a successful network connection is separate from compatibility
with historical Wii servers or modern TLS endpoints.
## Validation and limits
Automated tests exercise descriptors, control requests, PHY/MAC registers, USB
address timing, link notifications, split Ethernet frames, OHCI DMA enumeration,
EHCI companion routing and actual libslirp ARP/DHCP replies. Runtime tests skip when
the optional library directory is absent; a present but unloadable library fails.
The interrupt endpoint refreshes unchanged link/PHY status after 10 emulated USB
frames (the advertised full-speed interval), in addition to link-change reports.
IOS synchronously polls this endpoint: replying only once per link change leaves
later connectivity checks waiting indefinitely. Reports are coalesced, held while
software owns MDIO, and timed by OHCI0 frames rather than host time. Regression
tests cover repeated reads through OHCI DMA, pacing, MDIO ownership and savestates.
Winsock polling maps libslirp's normal/urgent reads to `POLLRDNORM`/`POLLRDBAND`.
Passing POSIX-style `POLLPRI` to Windows `WSAPoll` fails the entire poll with
`WSAEINVAL` (10022): DHCP/DNS and the TCP handshake can succeed while subsequent
response reads stall. A localhost TCP regression checks data and peer-close
delivery with the actual event mapping. Poll failures are logged with a bounded
error count instead of silently losing host readiness notifications.
These tests do **not** by themselves establish Internet connectivity from retail IOS.
That requires enumerating the adapter with the real IOS driver, configuring a wired
connection, then observing DNS and TCP traffic in an end-to-end test.
### Shop connection diagnostics (September 2026)
The wired connection test passed in the isolated test profile. The subsequent Shop
connection stalled inside native IOS `SSL_DOHANDSHAKE`, with unchanged network frame
counters. Sampling attributed most of one saturated host core to ARM JIT execution
and IOS memory accesses, not the NAT worker. Native random-number requests repeatedly
returned zero because AES DMA to `0x0d40f080` was incorrectly subject to the CPU's SRAM
bank swap. Device DMA now uses the physical SRAM layout; CPU mapping is unchanged.
Automated AES, SHA, DMA-boundary, OHCI and NAND tests cover this distinction.
On 2026-09-12, the user confirmed successful connection and navigation to the Shop's
Wii Channels listing using native IOS and the virtual Ethernet adapter. The supplied
capture shows 100% emulation speed and 59.96 FPS on that screen. This validates that
connection path, not downloads, purchases, all servers, or constant performance in
every channel. The targeted regression run passed 164 tests across 21 suites.
Separately, aligned Thumb bus accesses now call the exact bus helpers without an
interpreter/dispatcher round trip. Unaligned accesses retain their architectural
fallback. The targeted four-million-instruction SRAM benchmark measured a median
85.177 ms before and 54.051 ms after (about 1.58x throughput); this is not a measured
whole-channel FPS improvement.
### Remaining hardware limits
- USB **full-speed**, with 64-byte bulk packets through OHCI0. The existing EHCI
skeleton does not execute high-speed queue heads. This is not a completed USB 2.0
high-speed implementation or a cycle-accurate AX88772 model.
- PHY reset/autonegotiation completes synchronously; there is no physical cable
negotiation or USB link bandwidth model. Multicast hash filtering is not yet exact.
- The guest device is serialized in savestates, but restoring a state recreates NAT
and closes existing host TCP/UDP flows. Ethernet must be enabled consistently when
saving and restoring. This change advances Dolphin's savestate version.
- No Wi-Fi emulation, live adapter hotplug, bridged LAN broadcast discovery, or
persistent external EEPROM image is provided.
`IOS_USB` logs show address/configuration, receive/medium setup, and unsupported
AX88772 control requests. `IOS_NET` logs show runtime initialization and NAT errors.
The first 128 IPv4 frames per direction also log IP endpoints, protocol, lengths
and TCP flags/sequence numbers or UDP ports/ICMP type. Packet payloads are not logged.
The `-Ethernet` launcher flag enables both at the normal info level, without packet
payload logging. All NAT input, socket polling and callbacks run on the emulation
thread; callbacks never access guest RAM directly. Polls are nonblocking and timed
from the emulated ARM clock.
## References
- [IOS OHCI known devices](https://wiibrew.org/wiki//dev/usb/oh0#Known_Devices)
- [ASIX AX88772 datasheet, EEPROM and MDIO register layouts](https://www.framboise314.fr/wp-content/uploads/2016/08/AX88772.pdf)
- [Linux ASIX register definitions](https://github.com/torvalds/linux/blob/master/drivers/net/usb/asix.h)
- [Linux ASIX device initialization](https://github.com/torvalds/linux/blob/master/drivers/net/usb/asix_devices.c)
- [Linux ASIX frame handling](https://github.com/torvalds/linux/blob/master/drivers/net/usb/asix_common.c)
- [libslirp API](https://gitlab.freedesktop.org/slirp/libslirp/-/blob/v4.9.3/src/libslirp.h)
- [Microsoft WSAPoll supported event flags](https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsapoll)
- [MSYS2 libslirp package](https://packages.msys2.org/package/mingw-w64-ucrt-x86_64-libslirp)