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