From b931671fde1e0461cbca37eeb8348a25d44ab76e Mon Sep 17 00:00:00 2001 From: Yaya48 Date: Sat, 12 Sep 2026 11:08:25 +0200 Subject: [PATCH] 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. --- Externals/libslirp/COPYRIGHT | 33 + Externals/libslirp/include/libslirp-version.h | 24 + Externals/libslirp/include/libslirp.h | 426 +++++++++++++ Externals/libslirp/runtime-msys2.json | 42 ++ Run-Wii-IOS-LLE.ps1 | 10 + Setup-Wii-LLE-Network.ps1 | 39 ++ Source/Core/Core/CMakeLists.txt | 6 + Source/Core/Core/Config/MainSettings.cpp | 1 + Source/Core/Core/Config/MainSettings.h | 1 + Source/Core/Core/HW/WII_IPC.h | 6 +- Source/Core/Core/IOS/Starlet/ARMJitX64.cpp | 83 ++- Source/Core/Core/IOS/Starlet/ARMJitX64.h | 14 +- Source/Core/Core/IOS/Starlet/AX88772.cpp | 579 +++++++++++++++++ Source/Core/Core/IOS/Starlet/AX88772.h | 81 +++ Source/Core/Core/IOS/Starlet/SlirpNetwork.cpp | 320 ++++++++++ Source/Core/Core/IOS/Starlet/SlirpNetwork.h | 38 ++ .../Core/Core/IOS/Starlet/StarletMemory.cpp | 219 ++++++- Source/Core/Core/IOS/Starlet/StarletMemory.h | 8 + Source/Core/Core/State.cpp | 2 +- Source/UnitTests/Core/CMakeLists.txt | 2 + .../Core/IOS/Starlet/ARMCoreTest.cpp | 344 ++++++++++ .../Core/IOS/Starlet/AX88772Test.cpp | 600 ++++++++++++++++++ docs/Wii_IOS_LLE.md | 18 +- docs/Wii_LLE_Ethernet.md | 120 ++++ 24 files changed, 2952 insertions(+), 64 deletions(-) create mode 100644 Externals/libslirp/COPYRIGHT create mode 100644 Externals/libslirp/include/libslirp-version.h create mode 100644 Externals/libslirp/include/libslirp.h create mode 100644 Externals/libslirp/runtime-msys2.json create mode 100644 Setup-Wii-LLE-Network.ps1 create mode 100644 Source/Core/Core/IOS/Starlet/AX88772.cpp create mode 100644 Source/Core/Core/IOS/Starlet/AX88772.h create mode 100644 Source/Core/Core/IOS/Starlet/SlirpNetwork.cpp create mode 100644 Source/Core/Core/IOS/Starlet/SlirpNetwork.h create mode 100644 Source/UnitTests/Core/IOS/Starlet/AX88772Test.cpp create mode 100644 docs/Wii_LLE_Ethernet.md diff --git a/Externals/libslirp/COPYRIGHT b/Externals/libslirp/COPYRIGHT new file mode 100644 index 0000000000..9ee1e99101 --- /dev/null +++ b/Externals/libslirp/COPYRIGHT @@ -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. diff --git a/Externals/libslirp/include/libslirp-version.h b/Externals/libslirp/include/libslirp-version.h new file mode 100644 index 0000000000..43351b1dfd --- /dev/null +++ b/Externals/libslirp/include/libslirp-version.h @@ -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_ */ diff --git a/Externals/libslirp/include/libslirp.h b/Externals/libslirp/include/libslirp.h new file mode 100644 index 0000000000..240dfe8bc5 --- /dev/null +++ b/Externals/libslirp/include/libslirp.h @@ -0,0 +1,426 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +#ifndef LIBSLIRP_H +#define LIBSLIRP_H + +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#include +#include +#include + +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 +typedef ssize_t slirp_ssize_t; +#include +#include +#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 */ diff --git a/Externals/libslirp/runtime-msys2.json b/Externals/libslirp/runtime-msys2.json new file mode 100644 index 0000000000..457f23cb9f --- /dev/null +++ b/Externals/libslirp/runtime-msys2.json @@ -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" + } +] diff --git a/Run-Wii-IOS-LLE.ps1 b/Run-Wii-IOS-LLE.ps1 index 3abe4176d9..ef63dbd79f 100644 --- a/Run-Wii-IOS-LLE.ps1 +++ b/Run-Wii-IOS-LLE.ps1 @@ -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 diff --git a/Setup-Wii-LLE-Network.ps1 b/Setup-Wii-LLE-Network.ps1 new file mode 100644 index 0000000000..c4f83a673d --- /dev/null +++ b/Setup-Wii-LLE-Network.ps1 @@ -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" diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 00c4ca9969..8b8a73b61f 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -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) diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 9ae5d6f1de..676de1278a 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -258,6 +258,7 @@ const Info MAIN_GPU_DETERMINISM_MODE{{System::Main, "Core", "GPUDet const Info MAIN_OVERRIDE_BOOT_IOS{{System::Main, "Core", "OverrideBootIOS"}, -1}; const Info MAIN_WII_IOS_LLE{{System::Main, "Core", "WiiIOSLLE"}, false}; const Info MAIN_WII_STARLET_JIT{{System::Main, "Core", "WiiStarletJIT"}, true}; +const Info MAIN_WII_LLE_ETHERNET{{System::Main, "Core", "WiiLLEEthernet"}, false}; GPUDeterminismMode GetGPUDeterminismMode() { diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index aa804b6010..0dfdf27545 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -168,6 +168,7 @@ extern const Info MAIN_REAL_WII_REMOTE_REPEAT_REPORTS; extern const Info MAIN_OVERRIDE_BOOT_IOS; extern const Info MAIN_WII_IOS_LLE; extern const Info MAIN_WII_STARLET_JIT; +extern const Info MAIN_WII_LLE_ETHERNET; extern const Info MAIN_WII_NUS_SHOP_URL; extern const Info MAIN_WII_WIILINK_ENABLE; diff --git a/Source/Core/Core/HW/WII_IPC.h b/Source/Core/Core/HW/WII_IPC.h index 1d40b68a73..d3ff168560 100644 --- a/Source/Core/Core/HW/WII_IPC.h +++ b/Source/Core/Core/HW/WII_IPC.h @@ -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 diff --git a/Source/Core/Core/IOS/Starlet/ARMJitX64.cpp b/Source/Core/Core/IOS/Starlet/ARMJitX64.cpp index 381d840a11..589a28ae0e 100644 --- a/Source/Core/Core/IOS/Starlet/ARMJitX64.cpp +++ b/Source/Core/Core/IOS/Starlet/ARMJitX64.cpp @@ -76,10 +76,10 @@ ARMJitX64::ARMJitX64(ARMCore& core) : m_core(core) m_executed_instructions_offset = static_cast(reinterpret_cast(&m_core.m_executed_instructions) - base); m_control_offset = static_cast(reinterpret_cast(&m_core.m_cp15.control) - base); - m_translation_table_base_offset = static_cast( - reinterpret_cast(&m_core.m_cp15.translation_table_base) - base); - m_domain_access_control_offset = static_cast( - reinterpret_cast(&m_core.m_cp15.domain_access_control) - base); + m_translation_table_base_offset = + static_cast(reinterpret_cast(&m_core.m_cp15.translation_table_base) - base); + m_domain_access_control_offset = + static_cast(reinterpret_cast(&m_core.m_cp15.domain_access_control) - base); m_process_id_offset = static_cast(reinterpret_cast(&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(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(2 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))), + CMP(32, MDisp(R11, static_cast(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(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(3 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))), + CMP(32, MDisp(R11, static_cast(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(3 * sizeof(FastEntry)))); @@ -331,15 +329,13 @@ void ARMJitX64::GenerateDispatcher() SetJumpTarget(mmu_way1_key_miss); CMP(32, MDisp(R11, static_cast(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(2 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))), + CMP(32, MDisp(R11, static_cast(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(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(3 * sizeof(FastEntry) + offsetof(FastEntry, physical_page))), + CMP(32, MDisp(R11, static_cast(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(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* slow_paths, u32 acc } void ARMJitX64::EmitThumbMemorySlowPath(const std::vector& 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(value)); return; } + if (access_size == 2) + { + const u16 halfword = static_cast(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); diff --git a/Source/Core/Core/IOS/Starlet/ARMJitX64.h b/Source/Core/Core/IOS/Starlet/ARMJitX64.h index 86bd9e64c4..a38296237a 100644 --- a/Source/Core/Core/IOS/Starlet/ARMJitX64.h +++ b/Source/Core/Core/IOS/Starlet/ARMJitX64.h @@ -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& 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& slow_paths, u32 instruction, u32 address, Gen::FixupBranch direct_done); void EmitThumbAddSub(u16 instruction); diff --git a/Source/Core/Core/IOS/Starlet/AX88772.cpp b/Source/Core/Core/IOS/Starlet/AX88772.cpp new file mode 100644 index 0000000000..c5b1e6619c --- /dev/null +++ b/Source/Core/Core/IOS/Starlet/AX88772.cpp @@ -0,0 +1,579 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/IOS/Starlet/AX88772.h" + +#include +#include +#include + +#include + +#include "Common/ChunkFile.h" +#include "Common/Logging/Log.h" + +namespace IOS::LLE +{ +namespace +{ +u16 LE16(std::span bytes, size_t offset = 0) +{ + return u16(bytes[offset]) | (u16(bytes[offset + 1]) << 8); +} +std::vector Word(u16 value) +{ + return {static_cast(value), static_cast(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 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 strings{"ASIX", "AX88772A USB Ethernet", + "DolphinLLE0001"}; + const u8 index = value & 0xff; + if (index > strings.size()) + return {}; + const auto str = strings[index - 1]; + std::vector descriptor{static_cast(2 + str.size() * 2), 3}; + for (char c : str) + { + descriptor.push_back(static_cast(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(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(m_eeprom[0x11] >> 8), static_cast(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 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(value); + INFO_LOG_FMT(IOS_USB, "AX88772 assigned USB address {}", value); + return true; + case 9: + if (value > 1) + return false; + m_configuration = static_cast(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(value), static_cast(value >> 8), static_cast(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(value); + break; + case 0x1f: + m_gpio = value; + break; + case 0x20: + m_software_reset = static_cast(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(value); + break; + default: + return false; + } + return true; +} + +AX88772::Result AX88772::BulkOut(std::span 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(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 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(frame.size()); + auto packet = Word(length); + auto inverse = Word(static_cast(~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 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 event{0xa1, + 0, + static_cast(LinkUp() ? 1 : 0), + 0, + static_cast(first), + static_cast(first >> 8), + static_cast(second), + static_cast(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 diff --git a/Source/Core/Core/IOS/Starlet/AX88772.h b/Source/Core/Core/IOS/Starlet/AX88772.h new file mode 100644 index 0000000000..f627d0eb23 --- /dev/null +++ b/Source/Core/Core/IOS/Starlet/AX88772.h @@ -0,0 +1,81 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#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)>; + 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 frame); + Result Transfer(u8 endpoint, u8 direction, std::span buffer, size_t* actual); + u8 GetAddress() const { return m_address; } + +private: + static constexpr u8 INTERRUPT_INTERVAL_MS = 10; + bool PrepareControl(); + bool ApplyControl(std::span data); + std::vector Descriptor(u16 value) const; + u16 ReadPHY(u16 reg) const; + bool LinkUp() const; + Result BulkOut(std::span data); + + Transmit m_transmit; + std::array m_mac{0x02, 0x44, 0x4f, 0x4c, 0x00, 0x01}; + std::array m_eeprom{}; + std::array m_phy{}; + std::array m_multicast{}; + std::array m_ipg{0x15, 0x0c, 0x12}; + std::array m_setup{}; + std::vector m_control; + std::vector m_out; + std::vector m_tx; + std::deque> 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 diff --git a/Source/Core/Core/IOS/Starlet/SlirpNetwork.cpp b/Source/Core/Core/IOS/Starlet/SlirpNetwork.cpp new file mode 100644 index 0000000000..ca7b1cae5d --- /dev/null +++ b/Source/Core/Core/IOS/Starlet/SlirpNetwork.cpp @@ -0,0 +1,320 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/IOS/Starlet/SlirpNetwork.h" + +#include +#include +#include +#include +#include + +#define LIBSLIRP_STATIC +#include "libslirp.h" +#ifndef _WIN32 +#include +#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 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 bytes, size_t offset) { + return (u16(bytes[offset]) << 8) | bytes[offset + 1]; + }; + const auto dword = [&](std::span 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(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 sockets; +#else + std::vector sockets; +#endif + struct Timer + { + SlirpTimerCb callback; + void* opaque; + s64 deadline = std::numeric_limits::max(); + }; + std::vector> 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(); + 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(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(data), length}, "RX", self.rx_frames); + self.receive({static_cast(data), length}); + } + return static_cast(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(opaque)->now_ns; }; + cb.timer_new = [](SlirpTimerCb callback, void* callback_opaque, void* opaque) -> void* { + auto& timers = static_cast(opaque)->timers; + timers.push_back(std::make_unique(Impl::Timer{callback, callback_opaque})); + return timers.back().get(); + }; + cb.timer_free = [](void* timer, void* opaque) { + auto& timers = static_cast(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(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 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(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((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(opaque)->sockets; + sockets.push_back({socket, SlirpSocketPolling::ToNativeEvents(events), 0}); + return static_cast(sockets.size() - 1); + }, + &self); + int result = 0; + if (!self.sockets.empty()) + { +#ifdef _WIN32 + result = WSAPoll(self.sockets.data(), static_cast(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(opaque)->sockets; + if (index < 0 || static_cast(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::max(); + callback(opaque); + } +} +} // namespace IOS::LLE diff --git a/Source/Core/Core/IOS/Starlet/SlirpNetwork.h b/Source/Core/Core/IOS/Starlet/SlirpNetwork.h new file mode 100644 index 0000000000..be6566e837 --- /dev/null +++ b/Source/Core/Core/IOS/Starlet/SlirpNetwork.h @@ -0,0 +1,38 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#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)>; + SlirpNetwork(); + ~SlirpNetwork(); + bool Start(const std::string& directory, Receive receive, std::string* error); + void Input(std::span frame); + void Poll(u64 arm_cycles); + +private: + struct Impl; + std::unique_ptr m_impl; +}; +} // namespace IOS::LLE diff --git a/Source/Core/Core/IOS/Starlet/StarletMemory.cpp b/Source/Core/Core/IOS/Starlet/StarletMemory.cpp index e7c01e559c..9bb92a0f93 100644 --- a/Source/Core/Core/IOS/Starlet/StarletMemory.cpp +++ b/Source/Core/Core/IOS/Starlet/StarletMemory.cpp @@ -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([this](std::span frame) { + if (m_ethernet_network) + m_ethernet_network->Input(frame); + }); + m_ethernet_network = std::make_unique(); + if (!m_ethernet_network->Start( + runtime_directory, [this](std::span 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 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(ReadRegister(base + OHCI_FRAME_NUMBER)); const u16 frame = static_cast(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(frame)); - WriteMapped8(hcca + 0x81, static_cast(frame >> 8)); + WriteDMA8(hcca + 0x80, static_cast(frame)); + WriteDMA8(hcca + 0x81, static_cast(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(ReadMapped8(address)) | static_cast(ReadMapped8(address + 1)) << 8 | - static_cast(ReadMapped8(address + 2)) << 16 | - static_cast(ReadMapped8(address + 3)) << 24; + return static_cast(ReadDMA8(address)) | static_cast(ReadDMA8(address + 1)) << 8 | + static_cast(ReadDMA8(address + 2)) << 16 | + static_cast(ReadDMA8(address + 3)) << 24; } void StarletMemory::WriteOHCIMemory32(u32 address, u32 value) { - WriteMapped8(address, static_cast(value)); - WriteMapped8(address + 1, static_cast(value >> 8)); - WriteMapped8(address + 2, static_cast(value >> 16)); - WriteMapped8(address + 3, static_cast(value >> 24)); + WriteDMA8(address, static_cast(value)); + WriteDMA8(address + 1, static_cast(value >> 8)); + WriteDMA8(address + 2, static_cast(value >> 16)); + WriteDMA8(address + 3, static_cast(value >> 24)); } std::vector StarletMemory::ReadOHCIBuffer(u32 current_buffer, u32 buffer_end) const @@ -1525,10 +1662,10 @@ std::vector StarletMemory::ReadOHCIBuffer(u32 current_buffer, u32 buffer_end std::vector buffer(first_size + second_size); for (size_t i = 0; i < first_size; ++i) - buffer[i] = ReadMapped8(current_buffer + static_cast(i)); + buffer[i] = ReadDMA8(current_buffer + static_cast(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(i)); + buffer[first_size + i] = ReadDMA8(second_page + static_cast(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(i), data[i]); + WriteDMA8(current_buffer + static_cast(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((buffer_end & 0xfff) + 1)); for (size_t i = 0; i < second_size; ++i) - WriteMapped8(second_page + static_cast(i), data[first_size + i]); + WriteDMA8(second_page + static_cast(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(endpoint), static_cast(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(i), data[i]); + WriteDMA8(dma_address + static_cast(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(i)); + data[i] = ReadDMA8(dma_address + static_cast(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 scr = {0x02, 0x05, 0, 0, 0, 0, 0, 0}; for (size_t i = 0; i < scr.size(); ++i) - WriteMapped8(dma_address + static_cast(i), scr[i]); + WriteDMA8(dma_address + static_cast(i), scr[i]); WriteRegister(SDHC_DMA_ADDRESS, dma_address + static_cast(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 input(size); std::vector output(size); for (size_t i = 0; i < size; ++i) - input[i] = ReadMapped8(source + static_cast(i)); + input[i] = ReadDMA8(source + static_cast(i)); std::array 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(i), output[i]); + WriteDMA8(destination + static_cast(i), output[i]); WriteRegister(AES_SRC, source + static_cast(size)); WriteRegister(AES_DEST, destination + static_cast(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) diff --git a/Source/Core/Core/IOS/Starlet/StarletMemory.h b/Source/Core/Core/IOS/Starlet/StarletMemory.h index ec3c4c5268..72eab31257 100644 --- a/Source/Core/Core/IOS/Starlet/StarletMemory.h +++ b/Source/Core/Core/IOS/Starlet/StarletMemory.h @@ -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 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 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 m_nand_journal; + std::unique_ptr m_ethernet; + std::unique_ptr m_ethernet_network; }; } // namespace IOS::LLE diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp index 0aa77abc3c..7ba8a51965 100644 --- a/Source/Core/Core/State.cpp +++ b/Source/Core/Core/State.cpp @@ -97,7 +97,7 @@ struct CompressAndDumpStateArgs static Common::WorkQueueThreadSP 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 diff --git a/Source/UnitTests/Core/CMakeLists.txt b/Source/UnitTests/Core/CMakeLists.txt index 2817fb88ad..ff2388c789 100644 --- a/Source/UnitTests/Core/CMakeLists.txt +++ b/Source/UnitTests/Core/CMakeLists.txt @@ -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) diff --git a/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp b/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp index 97d76c49a1..1ee20cc98e 100644 --- a/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp +++ b/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.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(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 key = {0x2b7e1516, 0x28aed2a6, 0xabf71588, 0x09cf4f3c}; + constexpr std::array iv = {0x00010203, 0x04050607, 0x08090a0b, 0x0c0d0e0f}; + constexpr std::array plaintext = {0x6bc1bee2, 0x2e409f96, 0xe93d7e11, 0x7393172a}; + constexpr std::array 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(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(i - 0x17ff0 + 1) : + i >= 0xfff0 && i < 0x10010 ? static_cast(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 initial = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, + 0xc3d2e1f0}; + constexpr std::array 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 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(INT_CAUSE_DI), 1u << 18); + EXPECT_EQ(static_cast(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((bus[offset + 1] << 8) | bus[offset])); + } + core.SetBigEndian(big_endian); + core.SetCPSR(static_cast(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(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::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 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((op >> 8) | (op << 8))); + } + bus.WriteARM(0x3fc, 0x81fe9273); + bus.WriteARM(0x400, 0xa5b6c7d8); + core.Reset(0x1000); + core.SetBigEndian(big_endian); + core.SetCPSR(static_cast(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(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) diff --git a/Source/UnitTests/Core/IOS/Starlet/AX88772Test.cpp b/Source/UnitTests/Core/IOS/Starlet/AX88772Test.cpp new file mode 100644 index 0000000000..7279072e88 --- /dev/null +++ b/Source/UnitTests/Core/IOS/Starlet/AX88772Test.cpp @@ -0,0 +1,600 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +#define LIBSLIRP_STATIC +#include "libslirp.h" + +#include "Common/ChunkFile.h" +#include "Common/FileUtil.h" +#include "Common/Network.h" +#include "Common/ScopeGuard.h" +#include "Core/Core.h" +#include "Core/IOS/Starlet/AX88772.h" +#include "Core/IOS/Starlet/SlirpNetwork.h" +#include "Core/IOS/Starlet/StarletMemory.h" +#include "Core/System.h" + +using IOS::LLE::AX88772; +using Result = AX88772::Result; +namespace +{ +std::array USBSetup(u8 type, u8 request, u16 value, u16 index, u16 length) +{ + return {type, + request, + static_cast(value), + static_cast(value >> 8), + static_cast(index), + static_cast(index >> 8), + static_cast(length), + static_cast(length >> 8)}; +} +std::vector Framed(std::span frame) +{ + const u16 length = static_cast(frame.size()); + std::vector result{static_cast(length), static_cast(length >> 8), + static_cast(~length), static_cast(~length >> 8)}; + result.insert(result.end(), frame.begin(), frame.end()); + return result; +} +std::vector Control(AX88772& device, u8 type, u8 request, u16 value, u16 index, + std::vector data = {}) +{ + auto setup = USBSetup(type, request, value, index, static_cast(data.size())); + size_t actual = 0; + EXPECT_EQ(device.Transfer(0, 0, setup, &actual), Result::Completed); + EXPECT_EQ(actual, 8u); + if (!data.empty()) + { + EXPECT_EQ(device.Transfer(0, (type & 0x80) ? 2 : 1, data, &actual), Result::Completed); + data.resize(actual); + } + EXPECT_EQ(device.Transfer(0, (type & 0x80) ? 1 : 2, {}, &actual), Result::Completed); + return data; +} +void Configure(AX88772& device) +{ + Control(device, 0, 9, 1, 0); + Control(device, 0x40, 0x10, 0x88, 0); + Control(device, 0x40, 0x1b, 0x306, 0); + device.SetLink(true); +} +std::string RuntimeDirectory() +{ + return File::GetExeDirectory() + "/../Network"; +} +} // namespace + +TEST(StarletAX88772, EnumeratesSupportedVIDPIDAndFullSpeedEndpoints) +{ + AX88772 device({}); + const auto descriptor = Control(device, 0x80, 6, 0x100, 0, std::vector(64)); + ASSERT_EQ(descriptor.size(), 18u); + EXPECT_EQ(descriptor[8], 0x95); + EXPECT_EQ(descriptor[9], 0x0b); + EXPECT_EQ(descriptor[10], 0x20); + EXPECT_EQ(descriptor[11], 0x77); + const auto config = Control(device, 0x80, 6, 0x200, 0, std::vector(255)); + ASSERT_EQ(config.size(), 39u); + EXPECT_EQ(config[20], 0x81); + EXPECT_EQ(config[27], 0x82); + EXPECT_EQ(config[29], 64); + EXPECT_EQ(config[34], 3); +} + +TEST(StarletAX88772, AddressChangesOnlyAfterStatusStage) +{ + AX88772 device({}); + size_t actual; + auto setup = USBSetup(0, 5, 7, 0, 0); + ASSERT_EQ(device.Transfer(0, 0, setup, &actual), Result::Completed); + EXPECT_EQ(device.GetAddress(), 0); + ASSERT_EQ(device.Transfer(0, 2, {}, &actual), Result::Completed); + EXPECT_EQ(device.GetAddress(), 7); + device.Reset(); + EXPECT_EQ(device.GetAddress(), 0); +} + +TEST(StarletAX88772, SplitControlReadsAdvanceRatherThanRepeat) +{ + AX88772 device({}); + size_t actual; + auto setup = USBSetup(0x80, 6, 0x100, 0, 18); + ASSERT_EQ(device.Transfer(0, 0, setup, &actual), Result::Completed); + std::array first{}; + std::array second{}; + ASSERT_EQ(device.Transfer(0, 2, first, &actual), Result::Completed); + EXPECT_EQ(actual, 8u); + ASSERT_EQ(device.Transfer(0, 2, second, &actual), Result::Completed); + EXPECT_EQ(actual, 10u); + EXPECT_EQ(second[0], 0x95); +} + +TEST(StarletAX88772, UnknownAndMalformedRequestsStall) +{ + AX88772 device({}); + size_t actual; + for (auto setup : {USBSetup(0xc0, 0xff, 0, 0, 2), USBSetup(0x40, 8, 0x10, 0, 3), + USBSetup(0x40, 0x14, 0, 0, 7), USBSetup(0x80, 6, 0x3ff, 0, 10)}) + EXPECT_EQ(device.Transfer(0, 0, setup, &actual), Result::Stalled); +} + +TEST(StarletAX88772, MACAndMIIRegistersRoundTrip) +{ + AX88772 device({}); + const std::vector mac{2, 3, 4, 5, 6, 7}; + Control(device, 0x40, 0x14, 0, 0, mac); + EXPECT_EQ(Control(device, 0xc0, 0x13, 0, 0, std::vector(6)), mac); + Control(device, 0x40, 0x08, 0x10, 4, {0xe1, 1}); + EXPECT_EQ(Control(device, 0xc0, 0x07, 0x10, 4, std::vector(2)), (std::vector{0xe1, 1})); + device.SetLink(true); + const auto status = Control(device, 0xc0, 0x07, 0x10, 1, std::vector(2)); + EXPECT_EQ(status[0] & 0x24, 0x24); +} + +TEST(StarletAX88772, LinkChangesRefreshStatusWithoutWaitingForPeriodicReport) +{ + AX88772 device({}); + Configure(device); + std::array event{}; + size_t actual; + EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed); + EXPECT_EQ(event[2], 1); + EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending); + device.SetLink(false); + EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed); + EXPECT_EQ(event[2], 0); +} + +TEST(StarletAX88772, UnchangedLinkGetsPeriodicStatusWithoutBusyPollingOrBacklog) +{ + AX88772 device({}); + Configure(device); + const auto config = Control(device, 0x80, 6, 0x200, 0, std::vector(255)); + ASSERT_EQ(config.size(), 39u); + const unsigned interval = config[24]; + ASSERT_EQ(interval, 10u); + std::array event{}; + size_t actual = 0; + for (unsigned report = 0; report < 3; ++report) + { + ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed); + EXPECT_EQ(actual, 8u); + EXPECT_EQ(event[2], 1); + for (unsigned frame = 0; frame < interval; ++frame) + { + for (unsigned poll = 0; poll < 3; ++poll) + { + EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending); + EXPECT_EQ(actual, 0u); + } + device.AdvanceUSBFrame(); + } + } + // Leaving the endpoint unpolled coalesces reports; it does not queue them. + for (unsigned frame = 0; frame < 1000; ++frame) + device.AdvanceUSBFrame(); + ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed); + EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending); +} + +TEST(StarletAX88772, PeriodicStatusWaitsForHardwareMDIOOwnership) +{ + AX88772 device({}); + Configure(device); + std::array event{}; + size_t actual = 0; + ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed); + Control(device, 0x40, 0x06, 0, 0); + for (unsigned frame = 0; frame < 20; ++frame) + device.AdvanceUSBFrame(); + EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending); + Control(device, 0x40, 0x0a, 0, 0); + ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed); + EXPECT_EQ(event, (std::array{0xa1, 0, 1, 0, 0x2d, 0x78, 0xe1, 0x45})); + EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending); +} + +TEST(StarletAX88772, StateRoundTripPreservesPeriodicStatusPhaseAndResetClearsIt) +{ + AX88772 device({}); + Configure(device); + std::array event{}; + size_t actual = 0; + ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed); + for (unsigned frame = 0; frame < 4; ++frame) + device.AdvanceUSBFrame(); + std::vector state(65536); + u8* ptr = state.data(); + PointerWrap writer(&ptr, state.size(), PointerWrap::Mode::Write); + device.DoState(writer); + ASSERT_TRUE(writer.IsWriteMode()); + const size_t size = ptr - state.data(); + AX88772 restored({}); + ptr = state.data(); + PointerWrap reader(&ptr, size, PointerWrap::Mode::Read); + restored.DoState(reader); + ASSERT_TRUE(reader.IsReadMode()); + for (unsigned frame = 0; frame < 6; ++frame) + { + EXPECT_EQ(restored.Transfer(1, 2, event, &actual), Result::Pending); + restored.AdvanceUSBFrame(); + } + ASSERT_EQ(restored.Transfer(1, 2, event, &actual), Result::Completed); + EXPECT_EQ(event[2], 1); + restored.Reset(); + EXPECT_EQ(restored.Transfer(1, 2, event, &actual), Result::Stalled); + Configure(restored); + EXPECT_EQ(restored.Transfer(1, 2, event, &actual), Result::Completed); +} + +TEST(StarletAX88772, EEPROMAndPHYAddressRegisterDescribeTheSameHardware) +{ + AX88772 device({}); + const auto eeprom = Control(device, 0xc0, 0x0b, 0x11, 0, std::vector(2)); + EXPECT_EQ(eeprom, (std::vector{0x10, 0xe0})); + EXPECT_EQ(Control(device, 0xc0, 0x19, 0, 0, std::vector(2)), (std::vector{0xe0, 0x10})); + // Accessing an absent PHY must not poison the USB control endpoint. + Control(device, 0x40, 0x08, 0xff, 4, {0, 0}); + EXPECT_EQ(Control(device, 0xc0, 0x07, 0xff, 4, std::vector(2)), + (std::vector{0xff, 0xff})); + // Only the low five bits reach MDIO, as specified by the device datasheet. + Control(device, 0x40, 0x08, 0xf0, 0x24, {0x23, 1}); + EXPECT_EQ(Control(device, 0xc0, 0x07, 0x10, 4, std::vector(2)), (std::vector{0x23, 1})); +} + +TEST(StarletAX88772, InterruptIncludesConfiguredPHYRegistersAndWaitsForMDIOOwnership) +{ + AX88772 device({}); + Configure(device); + EXPECT_EQ(Control(device, 0xc0, 0x0b, 0x0f, 0, std::vector(2)), (std::vector{5, 1})); + EXPECT_EQ(Control(device, 0xc0, 0x0b, 0x10, 0, std::vector(2)), (std::vector{0xee, 5})); + Control(device, 0x40, 0x06, 0, 0); + std::array event{}; + size_t actual; + EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending); + Control(device, 0x40, 0x0a, 0, 0); + ASSERT_EQ(device.Transfer(1, 2, event, &actual), Result::Completed); + EXPECT_EQ(event, (std::array{0xa1, 0, 1, 0, 0x2d, 0x78, 0xe1, 0x45})); + EXPECT_EQ(device.Transfer(1, 2, event, &actual), Result::Pending); +} + +TEST(StarletAX88772, TransmitsOddLengthFramesAcrossUSBTransfers) +{ + std::vector> received; + AX88772 device( + [&](std::span frame) { received.emplace_back(frame.begin(), frame.end()); }); + Configure(device); + std::vector frame(61, 0x42); + auto packet = Framed(frame); + size_t actual; + EXPECT_EQ(device.Transfer(3, 1, std::span(packet).first(17), &actual), Result::Completed); + EXPECT_TRUE(received.empty()); + EXPECT_EQ(device.Transfer(3, 1, std::span(packet).subspan(17), &actual), Result::Completed); + ASSERT_EQ(received.size(), 1u); + EXPECT_EQ(received[0], frame); + packet[2] ^= 1; + EXPECT_EQ(device.Transfer(3, 1, packet, &actual), Result::Stalled); + EXPECT_EQ(received.size(), 1u); +} + +TEST(StarletAX88772, ReceiveFramingAndResetPreservePacketBoundaries) +{ + AX88772 device({}); + Configure(device); + std::vector frame(61, 0x42); + std::fill_n(frame.begin(), 6, 0xff); + device.ReceiveFrame(frame); + std::array buffer{}; + size_t actual; + ASSERT_EQ(device.Transfer(2, 2, buffer, &actual), Result::Completed); + EXPECT_EQ(actual, 66u); + EXPECT_EQ(buffer[0], 61); + EXPECT_EQ(buffer[2], static_cast(~61)); + EXPECT_TRUE(std::equal(frame.begin(), frame.end(), buffer.begin() + 4)); + EXPECT_EQ(device.Transfer(2, 2, buffer, &actual), Result::Pending); + device.ReceiveFrame(frame); + device.Reset(); + EXPECT_EQ(device.Transfer(2, 2, buffer, &actual), Result::Stalled); + Configure(device); + EXPECT_EQ(device.Transfer(2, 2, buffer, &actual), Result::Pending); +} + +TEST(StarletEthernetNAT, PollEventMappingPreservesReadWritePriorityAndErrors) +{ + using namespace IOS::LLE::SlirpSocketPolling; + for (int flags = 0; flags < 8; ++flags) + EXPECT_EQ(ToSlirpEvents(ToNativeEvents(flags)), flags); + EXPECT_EQ(ToSlirpEvents(POLLERR | POLLHUP), SLIRP_POLL_ERR | SLIRP_POLL_HUP); + EXPECT_EQ(ToSlirpEvents(POLLNVAL), SLIRP_POLL_ERR); +#ifdef _WIN32 + EXPECT_EQ(ToNativeEvents(SLIRP_POLL_IN), POLLRDNORM); + EXPECT_EQ(ToNativeEvents(SLIRP_POLL_PRI), POLLRDBAND); + EXPECT_EQ(ToNativeEvents(SLIRP_POLL_IN | SLIRP_POLL_PRI) & POLLPRI, 0); +#endif +} + +#ifdef _WIN32 +TEST(StarletEthernetNAT, WinsockTCPReadAndPeerCloseWorkWithSlirpRequestedEvents) +{ + using namespace IOS::LLE::SlirpSocketPolling; + WSADATA wsadata{}; + ASSERT_EQ(WSAStartup(MAKEWORD(2, 2), &wsadata), 0); + Common::ScopeGuard cleanup([] { WSACleanup(); }); + const SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + ASSERT_NE(listener, INVALID_SOCKET); + Common::ScopeGuard close_listener([&] { closesocket(listener); }); + sockaddr_in address{}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + ASSERT_EQ(bind(listener, reinterpret_cast(&address), sizeof(address)), 0); + ASSERT_EQ(listen(listener, 1), 0); + int address_size = sizeof(address); + ASSERT_EQ(getsockname(listener, reinterpret_cast(&address), &address_size), 0); + const SOCKET client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + ASSERT_NE(client, INVALID_SOCKET); + Common::ScopeGuard close_client([&] { closesocket(client); }); + ASSERT_EQ(connect(client, reinterpret_cast(&address), sizeof(address)), 0); + u_long nonblocking = 1; + ASSERT_EQ(ioctlsocket(client, FIONBIO, &nonblocking), 0); + const SOCKET server = accept(listener, nullptr, nullptr); + ASSERT_NE(server, INVALID_SOCKET); + Common::ScopeGuard close_server([&] { closesocket(server); }); + ASSERT_EQ(send(server, "test", 4, 0), 4); + ASSERT_EQ(shutdown(server, SD_SEND), 0); + // libslirp adds PRI immediately after a TCP connection is established. An + // unsupported Winsock event here used to prevent receiving any data or FIN. + WSAPOLLFD fd{client, ToNativeEvents(SLIRP_POLL_IN | SLIRP_POLL_PRI), 0}; + ASSERT_EQ(WSAPoll(&fd, 1, 1000), 1) << WSAGetLastError(); + EXPECT_NE(ToSlirpEvents(fd.revents) & SLIRP_POLL_IN, 0); + std::array data{}; + ASSERT_EQ(recv(client, data.data(), static_cast(data.size()), 0), 4); + EXPECT_EQ(data, (std::array{'t', 'e', 's', 't'})); + fd.revents = 0; + ASSERT_EQ(WSAPoll(&fd, 1, 1000), 1) << WSAGetLastError(); + EXPECT_NE(ToSlirpEvents(fd.revents) & (SLIRP_POLL_IN | SLIRP_POLL_HUP), 0); + EXPECT_EQ(recv(client, data.data(), static_cast(data.size()), 0), 0); +} +#endif + +TEST(StarletEthernetNAT, MissingRuntimeFailsExplicitly) +{ + IOS::LLE::SlirpNetwork network; + std::string error; + EXPECT_FALSE(network.Start("/nonexistent-dolphin-slirp-runtime", {}, &error)); + EXPECT_FALSE(error.empty()); +} + +TEST(StarletEthernetNAT, LibslirpAnswersARPThroughAX88772BulkEndpoints) +{ + if (!File::IsDirectory(RuntimeDirectory())) + GTEST_SKIP() << "Optional local libslirp runtime absent"; + IOS::LLE::SlirpNetwork network; + AX88772 device([&](std::span frame) { network.Input(frame); }); + std::string error; + ASSERT_TRUE(network.Start( + RuntimeDirectory(), [&](std::span frame) { device.ReceiveFrame(frame); }, &error)) + << error; + Configure(device); + Common::MACAddress mac{2, 0x44, 0x4f, 0x4c, 0, 1}; + Common::ARPPacket request(Common::MACAddress{255, 255, 255, 255, 255, 255}, mac); + request.arp_header.opcode = htons(1); + request.arp_header.sender_address = mac; + request.arp_header.sender_ip = htonl(0x0a00020f); + request.arp_header.target_ip = htonl(0x0a000202); + auto framed = Framed(request.Build()); + size_t actual; + ASSERT_EQ(device.Transfer(3, 1, framed, &actual), Result::Completed); + network.Poll(243000); + std::array buffer{}; + ASSERT_EQ(device.Transfer(2, 2, buffer, &actual), Result::Completed); + ASSERT_GE(actual, 46u); + const auto arp = + Common::PacketView(buffer.data() + 4, buffer[0] | (buffer[1] << 8)).GetARPPacket(); + ASSERT_TRUE(arp.has_value()); + EXPECT_EQ(ntohs(arp->arp_header.opcode), 2); + EXPECT_EQ(arp->arp_header.sender_ip, htonl(0x0a000202)); +} + +TEST(StarletEthernetNAT, LibslirpDHCPProvidesAddressRouterAndDNS) +{ + if (!File::IsDirectory(RuntimeDirectory())) + GTEST_SKIP() << "Optional local libslirp runtime absent"; + IOS::LLE::SlirpNetwork network; + std::vector> replies; + std::string error; + ASSERT_TRUE(network.Start( + RuntimeDirectory(), + [&](std::span frame) { replies.emplace_back(frame.begin(), frame.end()); }, &error)) + << error; + Common::MACAddress mac{2, 0x44, 0x4f, 0x4c, 0, 1}; + Common::DHCPPacket discover; + discover.body.message_type = 1; + discover.body.hardware_type = 1; + discover.body.hardware_addr = 6; + discover.body.transaction_id = htonl(0x12345678); + discover.body.boot_flag = htons(0x8000); + discover.body.client_mac = mac; + discover.AddOption(53, {1}); + discover.AddOption(55, {1, 3, 6}); + sockaddr_in from{}, to{}; + from.sin_port = htons(68); + to.sin_addr.s_addr = 0xffffffff; + to.sin_port = htons(67); + Common::UDPPacket packet(Common::MACAddress{255, 255, 255, 255, 255, 255}, mac, from, to, + discover.Build()); + network.Input(packet.Build()); + network.Poll(243000); + ASSERT_FALSE(replies.empty()); + const auto udp = Common::PacketView(replies.back().data(), replies.back().size()).GetUDPPacket(); + ASSERT_TRUE(udp.has_value()); + ASSERT_GE(udp->data.size(), Common::DHCPBody::SIZE); + Common::DHCPPacket offer(udp->data); + EXPECT_EQ(offer.body.transaction_id, discover.body.transaction_id); + EXPECT_EQ(offer.body.your_ip, htonl(0x0a00020f)); + EXPECT_TRUE(std::ranges::any_of(offer.options, [](const auto& option) { + return option == std::vector{3, 4, 10, 0, 2, 2}; + })); + EXPECT_TRUE(std::ranges::any_of(offer.options, [](const auto& option) { + return option == std::vector{6, 4, 10, 0, 2, 3}; + })); +} + +TEST(StarletEthernetNAT, ExternalOHCIEnumeratesDeviceWithoutTouchingBluetooth) +{ + if (!File::IsDirectory(RuntimeDirectory())) + GTEST_SKIP() << "Optional local libslirp runtime absent"; + Core::DeclareAsCPUThread(); + IOS::LLE::StarletMemory memory(Core::System::GetInstance()); + memory.Reset(); + std::string error; + ASSERT_TRUE(memory.EnableEthernetNAT(RuntimeDirectory(), &error)) << error; + constexpr u32 ohci = 0x0d050000, hcca = 0x0d404000, ed = hcca + 0x100; + constexpr u32 setup_td = hcca + 0x200, data_td = hcca + 0x210, status_td = hcca + 0x220, + tail = hcca + 0x230; + constexpr u32 setup_buffer = hcca + 0x300, data_buffer = hcca + 0x400; + const auto put = [&](u32 addr, u32 value) { + for (u32 i = 0; i < 4; ++i) + memory.Write8(addr + i, static_cast(value >> (8 * i))); + }; + const auto get = [&](u32 addr) { + u32 value = 0; + for (u32 i = 0; i < 4; ++i) + value |= u32(memory.Read8(addr + i)) << (8 * i); + return value; + }; + EXPECT_NE(memory.Read32(ohci + 0x54) & 1, 0u); + const u32 bluetooth_before = memory.Read32(0x0d060054); + memory.Write32(ohci + 0x54, 2); // Enable connected port. + const auto setup = USBSetup(0x80, 6, 0x100, 0, 18); + for (u32 i = 0; i < 8; ++i) + memory.Write8(setup_buffer + i, setup[i]); + put(ed, 64u << 16); + put(ed + 4, tail); + put(ed + 8, setup_td); + put(ed + 12, 0); + put(setup_td, 0); + put(setup_td + 4, setup_buffer); + put(setup_td + 8, data_td); + put(setup_td + 12, setup_buffer + 7); + put(data_td, 2u << 19); + put(data_td + 4, data_buffer); + put(data_td + 8, status_td); + put(data_td + 12, data_buffer + 17); + put(status_td, 1u << 19); + put(status_td + 4, 0); + put(status_td + 8, tail); + put(status_td + 12, 0); + memory.Write32(ohci + 0x18, hcca); + memory.Write32(ohci + 0x20, ed); + memory.Write32(ohci + 4, 0x90); // Operational + control list. + EXPECT_EQ(get(ed + 8) & ~0xfu, tail); + EXPECT_EQ(memory.Read8(data_buffer + 8), 0x95); + EXPECT_EQ(memory.Read8(data_buffer + 10), 0x20); + EXPECT_EQ(memory.Read32(0x0d060054), bluetooth_before); +} + +TEST(StarletEthernetNAT, ExternalOHCICompletesRepeatedStatusReadsWithoutLinkChanges) +{ + if (!File::IsDirectory(RuntimeDirectory())) + GTEST_SKIP() << "Optional local libslirp runtime absent"; + Core::DeclareAsCPUThread(); + IOS::LLE::StarletMemory memory(Core::System::GetInstance()); + memory.Reset(); + std::string error; + ASSERT_TRUE(memory.EnableEthernetNAT(RuntimeDirectory(), &error)) << error; + constexpr u32 ohci = 0x0d050000, hcca = 0x0d404000, ed = hcca + 0x100; + constexpr u32 td = hcca + 0x200, status_td = td + 0x10, tail = td + 0x20; + constexpr u32 buffer = hcca + 0x300; + const auto put = [&](u32 addr, u32 value) { + for (u32 i = 0; i < 4; ++i) + memory.Write8(addr + i, static_cast(value >> (8 * i))); + }; + const auto get = [&](u32 addr) { + u32 value = 0; + for (u32 i = 0; i < 4; ++i) + value |= u32(memory.Read8(addr + i)) << (8 * i); + return value; + }; + // Configure through endpoint zero, then submit the same synchronous status + // reads used by IOS's Ethernet link polling thread. No guest code is bypassed. + memory.Write32(ohci + 0x54, 2); + const auto setup = USBSetup(0, 9, 1, 0, 0); + for (u32 i = 0; i < 8; ++i) + memory.Write8(buffer + i, setup[i]); + put(ed, 64u << 16); + put(ed + 4, tail); + put(ed + 8, td); + put(td, 0); + put(td + 4, buffer); + put(td + 8, status_td); + put(td + 12, buffer + 7); + put(status_td, 2u << 19); + put(status_td + 8, tail); + memory.Write32(ohci + 0x18, hcca); + memory.Write32(ohci + 0x20, ed); + memory.Write32(ohci + 4, 0x90); + ASSERT_EQ(get(ed + 8) & ~0xfu, tail); + memory.Write32(ohci + 0x0c, 2); // Acknowledge the configuration completion. + + for (u32 slot = 0; slot < 32; ++slot) + put(hcca + slot * 4, ed); + put(ed, (8u << 16) | (2u << 11) | (1u << 7)); + const auto rearm = [&] { + put(td, 0xf0000000u | (2u << 19)); + put(td + 4, buffer); + put(td + 8, tail); + put(td + 12, buffer + 7); + put(ed + 8, td); + }; + rearm(); + memory.Write32(ohci + 4, 0x84); // Operational + periodic list. + ASSERT_EQ(get(ed + 8) & ~0xfu, tail); + EXPECT_EQ(memory.Read8(buffer + 2), 1); + memory.Write32(ohci + 0x0c, 2); + rearm(); + constexpr u64 cycles_per_ms = 243000; + memory.AdvanceCycles(9 * cycles_per_ms); + EXPECT_EQ(get(ed + 8) & ~0xfu, td); + memory.AdvanceCycles(cycles_per_ms); + EXPECT_EQ(get(ed + 8) & ~0xfu, tail); + EXPECT_EQ(get(hcca + 0x84), td); + EXPECT_NE(memory.Read32(ohci + 0x0c) & 2, 0u); + EXPECT_EQ(memory.Read8(buffer + 2), 1); +} + +TEST(StarletEthernetNAT, EHCIHandsFullSpeedDeviceBackToExternalOHCI) +{ + if (!File::IsDirectory(RuntimeDirectory())) + GTEST_SKIP() << "Optional local libslirp runtime absent"; + Core::DeclareAsCPUThread(); + IOS::LLE::StarletMemory memory(Core::System::GetInstance()); + memory.Reset(); + std::string error; + ASSERT_TRUE(memory.EnableEthernetNAT(RuntimeDirectory(), &error)) << error; + constexpr u32 ehci = 0x0d040000, ohci_port = 0x0d050054; + EXPECT_NE(memory.Read32(ohci_port) & 1, 0u); + memory.Write32(ehci + 0x50, 1); // CONFIGFLAG claims the external ports. + EXPECT_EQ(memory.Read32(ohci_port) & 3, 0u); + EXPECT_NE(memory.Read32(ehci + 0x54) & 1, 0u); + memory.Write32(ehci + 0x54, 0x3000); // Port power + companion owner. + EXPECT_NE(memory.Read32(ohci_port) & 1, 0u); + memory.Write32(ehci + 0x54, 0x2000); // Power off. + EXPECT_EQ(memory.Read32(ohci_port) & 3, 0u); + memory.Write32(ehci + 0x54, 0x1100); // Powered reset detects full speed. + EXPECT_NE(memory.Read32(ehci + 0x54) & 0x2000, 0u); + EXPECT_NE(memory.Read32(ohci_port) & 1, 0u); +} diff --git a/docs/Wii_IOS_LLE.md b/docs/Wii_IOS_LLE.md index c0b94f4848..31f31bd6b6 100644 --- a/docs/Wii_IOS_LLE.md +++ b/docs/Wii_IOS_LLE.md @@ -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. diff --git a/docs/Wii_LLE_Ethernet.md b/docs/Wii_LLE_Ethernet.md new file mode 100644 index 0000000000..97282413d4 --- /dev/null +++ b/docs/Wii_LLE_Ethernet.md @@ -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)