mirror of
https://github.com/libretro/parallel-dc.git
synced 2026-11-08 17:27:41 -10:00
The modem: - V.42 error correction (LAPM) and V.42bis compression for the games that dial in sync mode, which were forced to async, and V.8bis and 56K (V.90/K56 speeds). - Receive and transmit FIFOs timed by the line's speeds, the TXFNF/TXHF status bits, and Power Smash's symmetric speed. - PPP waits for the game to speak first, on the network thread; this replaces the quarter-second delay on reads. - Its ID reports the country as USA, as upstream's. - Frames short or long, an XID with bad lengths, a REJ for a frame that is gone, a V.8bis message without its end, a V.42bis step-up of a dictionary not a power of two: none can read or write past a buffer, loop for ever or be refused wrongly any more. The Broadband Adapter could not be turned on: settings forced it off and nothing set it. The option "Expansion Port Device (Restart Required)" (reicast_network_device) chooses the modem, the adapter or nothing; the modem by default, as before. The adapter gets its link-change interrupt 10 ms after a reset, as KallistiOS waits for, frames under 6 or over 1514 bytes are dropped, and the network starts with the game's first frame and stops on a GAPS reset. DCNet: "Use DCNet" (reicast_dcnet, off by default) goes online through the dcnet.flyca.st relay instead of picoTCP, on the same network thread, non-blocking, without locks. Power Smash and Pro Yakyuu always go through it, as upstream has them. picoTCP: dns.flyca.st as the name server; a socket that is ready is read whole (an upload no longer stalls); the ports of Daytona, Sega Tetris, Golf Shiyouyo 2, Dee Dee Planet, Driving Strikers, Floigan and the Internet Game Pack. Stopping the network thread no longer waits for it. In the save state (V24), on a Dreamcast: the device chosen and what the modem (FIFOs, V.42, V.42bis, V.8bis) and the adapter (GAPS, its RAM, the RTL8139) hold, 34 KB. A live connection cannot be in a state: one loaded replays what came over the line at the times it came, and a line the state does not have hangs up, without waiting for the thread. tools/threads/pico_test.cpp tests the thread with the new interface, and DCNet against a stand-in relay.
1603 lines
44 KiB
C++
1603 lines
44 KiB
C++
/*
|
|
Created on: Sep 15, 2018
|
|
|
|
Copyright 2018 flyinghead
|
|
|
|
This file is part of reicast.
|
|
|
|
reicast is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
reicast is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with reicast. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#if defined(_WIN32) && !defined(FD_SETSIZE)
|
|
/* The pico thread waits on all of its sockets at once; Winsock's own
|
|
* limit for a select() is 64. */
|
|
#define FD_SETSIZE 256
|
|
#endif
|
|
#if !defined(_MSC_VER) && !defined(TARGET_NO_THREADS)
|
|
|
|
#include "stdclass.h"
|
|
#include "hw/sh4/sh4_sched.h"
|
|
|
|
#ifdef __MINGW32__
|
|
#define _POSIX_SOURCE
|
|
#endif
|
|
|
|
#ifdef __LIBRETRO__
|
|
#include <retro_timers.h>
|
|
#endif
|
|
|
|
extern "C" {
|
|
#include <pico_stack.h>
|
|
#include <pico_dev_ppp.h>
|
|
#include <pico_socket.h>
|
|
#include <pico_socket_tcp.h>
|
|
#include <pico_ipv4.h>
|
|
#include <pico_tcp.h>
|
|
#include <pico_dhcp_server.h>
|
|
}
|
|
|
|
#include "network/net_platform.h"
|
|
|
|
#include "types.h"
|
|
#include "picoppp.h"
|
|
#include "netservice.h"
|
|
#include "netthread.h"
|
|
#include "network/miniupnp.h"
|
|
#include "reios/reios.h"
|
|
|
|
#include <map>
|
|
#include <retro_spsc.h>
|
|
|
|
#define RESOLVER1_OPENDNS_COM "208.67.222.222"
|
|
#define AFO_ORIG_IP 0x83f2fb3f // 63.251.242.131 in network order
|
|
#define IGP_ORIG_IP 0xef2bd2cc // 204.210.43.239 in network order
|
|
|
|
static pico_device *pico_dev;
|
|
|
|
/* Bytes between the emulated modem (emulation thread) and the PPP stack
|
|
* (pico thread). Each ring has one writer and one reader and takes no
|
|
* lock. in_ring is kept small on purpose: when it fills, the stack waits
|
|
* for the modem to read, which is the flow control the link needs. */
|
|
#define IN_RING_BYTES 1024
|
|
#define OUT_RING_BYTES 16384
|
|
static retro_spsc_t in_ring; /* stack -> modem */
|
|
static retro_spsc_t out_ring; /* modem -> stack */
|
|
static bool rings_ready; /* emulation thread only */
|
|
/* Where the stack sleeps while in_ring is full. */
|
|
static cEventCount in_ring_ec;
|
|
/* Set by a network thread that does not sleep on in_ring_ec (DCNet) when
|
|
* in_ring had no room: the modem wakes it once it has read some. */
|
|
static retro_atomic_int_t in_ring_blocked;
|
|
|
|
/* Ethernet frames, for the broadband adapter. The adapter is emulated on
|
|
* the emulation thread and the stack runs on the pico thread, and neither
|
|
* may call into the other: the adapter's registers, its receive ring in
|
|
* guest memory and its interrupt belong to one, the stack's queues to the
|
|
* other. So a frame crosses as bytes in a ring, a 16-bit length and then
|
|
* the frame, written whole or not at all. */
|
|
#define ETH_RING_BYTES 65536
|
|
#define ETH_FRAME_MAX NET_ETH_FRAME_MAX
|
|
static retro_spsc_t eth_out_ring; /* adapter -> stack */
|
|
static retro_spsc_t eth_in_ring; /* stack -> adapter */
|
|
/* Set by the pico thread when the adapter's ring had no room for a frame;
|
|
* the emulation thread wakes it once it has taken some out. */
|
|
static retro_atomic_int_t eth_in_blocked;
|
|
|
|
/* What the thread is for, set before it starts: the broadband adapter
|
|
* rather than the modem, DCNet rather than picoTCP. */
|
|
static bool thread_bba;
|
|
static bool thread_dcnet;
|
|
/* PPP: the stack says nothing until the Dreamcast has sent something. What
|
|
* it sends before is dropped: Windows CE games, and a V.42 negotiation,
|
|
* would take it for line noise. (pico thread) */
|
|
static bool ppp_passive;
|
|
|
|
/* The pico thread does not tick on a timer. It waits, in select(), for the
|
|
* things that give it work: its native sockets, the stack's next timer,
|
|
* and this socket, which the emulation thread writes a byte to when it has
|
|
* handed the stack something (modem bytes, an Ethernet frame) or wants the
|
|
* thread to stop. It is a datagram socket on the loopback interface
|
|
* connected to itself.
|
|
*
|
|
* So that the emulation thread does not make a system call for every byte
|
|
* the modem sends, it only writes when the pico thread says it is waiting.
|
|
* pico_kicks counts what has been handed over: the pico thread notes it
|
|
* before it looks for work and, having said it is about to wait, does not
|
|
* wait if the count has moved since. */
|
|
static sock_t wake_sock = INVALID_SOCKET; /* made by the emulation thread before the pico thread starts */
|
|
static retro_atomic_int_t pico_waiting;
|
|
static retro_atomic_int_t pico_kicks;
|
|
|
|
static void wake_pico()
|
|
{
|
|
retro_atomic_fetch_add_int(&pico_kicks, 1);
|
|
if (retro_atomic_load_acquire_int(&pico_waiting))
|
|
send(wake_sock, "", 1, L_MSG_NOSIGNAL);
|
|
}
|
|
|
|
static pico_ip4 dcaddr;
|
|
static pico_ip4 dnsaddr;
|
|
static pico_socket *pico_tcp_socket, *pico_udp_socket;
|
|
|
|
static pico_ip4 afo_ip;
|
|
|
|
/* The most a native socket may be owed before the connection is given up. */
|
|
#define OUT_BUFFER_MAX (1024 * 1024)
|
|
|
|
struct socket_pair
|
|
{
|
|
socket_pair() : pico_sock(nullptr), native_sock(INVALID_SOCKET) {}
|
|
socket_pair(pico_socket *pico_sock, sock_t native_sock) : pico_sock(pico_sock), native_sock(native_sock) {}
|
|
~socket_pair() {
|
|
if (pico_sock != nullptr)
|
|
pico_socket_close(pico_sock);
|
|
if (native_sock != INVALID_SOCKET)
|
|
closesocket(native_sock);
|
|
}
|
|
socket_pair(socket_pair &&) = default;
|
|
socket_pair(const socket_pair&) = delete;
|
|
socket_pair& operator=(const socket_pair&) = delete;
|
|
|
|
pico_socket *pico_sock;
|
|
sock_t native_sock;
|
|
std::vector<char> in_buffer;
|
|
bool shutdown = false;
|
|
std::vector<char> out_buffer; // from the Dreamcast, not yet taken by the native socket
|
|
bool shut_wr = false; // the Dreamcast has closed its side: passed on once out_buffer has gone
|
|
bool reading = false; // read_from_dc_socket is taking what the Dreamcast sent: a close waits for it
|
|
|
|
/* Sends the native socket what it has not taken yet and then @len bytes
|
|
* more. What it does not take now is kept, and the thread is woken when
|
|
* the socket has room for it (see wait_for_work). False if the
|
|
* connection is lost, or more is waiting than a live peer would leave. */
|
|
bool send_native(const char *data, size_t len)
|
|
{
|
|
const bool kept = !out_buffer.empty();
|
|
|
|
if (kept)
|
|
{
|
|
out_buffer.insert(out_buffer.end(), data, data + len);
|
|
data = &out_buffer[0];
|
|
len = out_buffer.size();
|
|
}
|
|
int r = (int)send(native_sock, data, len, L_MSG_NOSIGNAL);
|
|
if (r < 0)
|
|
{
|
|
if (get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK)
|
|
return false;
|
|
r = 0;
|
|
}
|
|
if (kept)
|
|
out_buffer.erase(out_buffer.begin(), out_buffer.begin() + r);
|
|
else
|
|
out_buffer.assign(data + r, data + len);
|
|
pass_close();
|
|
return out_buffer.size() <= OUT_BUFFER_MAX;
|
|
}
|
|
|
|
/* Passes the Dreamcast's close on, once nothing it sent before closing
|
|
* is left to read or to send. */
|
|
void pass_close()
|
|
{
|
|
if (shut_wr && !reading && out_buffer.empty())
|
|
{
|
|
shut_wr = false;
|
|
if (native_sock != INVALID_SOCKET)
|
|
::shutdown(native_sock, SHUT_WR);
|
|
}
|
|
}
|
|
|
|
void receive_native()
|
|
{
|
|
size_t len;
|
|
const char *data;
|
|
char buf[536];
|
|
|
|
if (!in_buffer.empty())
|
|
{
|
|
len = in_buffer.size();
|
|
data = &in_buffer[0];
|
|
}
|
|
else
|
|
{
|
|
if (native_sock == INVALID_SOCKET)
|
|
{
|
|
if (!shutdown && pico_sock->q_out.size == 0)
|
|
{
|
|
pico_socket_shutdown(pico_sock, PICO_SHUT_RDWR);
|
|
shutdown = true;
|
|
}
|
|
return;
|
|
}
|
|
int r = (int)recv(native_sock, buf, sizeof(buf), 0);
|
|
if (r == 0)
|
|
{
|
|
INFO_LOG(MODEM, "Socket[%d] recv(%zd) returned 0 -> EOF", short_be(pico_sock->remote_port), sizeof(buf));
|
|
closesocket(native_sock);
|
|
native_sock = INVALID_SOCKET;
|
|
return;
|
|
}
|
|
if (r < 0)
|
|
{
|
|
if (get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK)
|
|
{
|
|
perror("recv tcp socket");
|
|
closesocket(native_sock);
|
|
native_sock = INVALID_SOCKET;
|
|
}
|
|
return;
|
|
}
|
|
len = r;
|
|
data = buf;
|
|
}
|
|
// What is waiting in in_buffer went through here when it was received
|
|
if (data == buf && pico_sock->remote_port == short_be(5011) && len >= 5)
|
|
{
|
|
// Visual Concepts sport games
|
|
if (buf[0] == 1)
|
|
memcpy(&buf[1], &pico_sock->local_addr.ip4.addr, 4);
|
|
}
|
|
|
|
int r2 = pico_socket_send(pico_sock, data, (int)len);
|
|
if (r2 < 0)
|
|
INFO_LOG(MODEM, "error TCP sending: %s", strerror(pico_err));
|
|
else if (r2 < (int)len)
|
|
{
|
|
if (r2 > 0 || in_buffer.empty())
|
|
{
|
|
len -= r2;
|
|
std::vector<char> remain(len);
|
|
memcpy(&remain[0], &data[r2], len);
|
|
std::swap(in_buffer, remain);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
in_buffer.clear();
|
|
}
|
|
}
|
|
};
|
|
|
|
// tcp sockets
|
|
static std::map<pico_socket *, socket_pair> tcp_sockets;
|
|
static std::map<pico_socket *, sock_t> tcp_connecting_sockets;
|
|
// udp sockets: src port -> socket fd
|
|
static std::map<uint16_t, sock_t> udp_sockets;
|
|
|
|
struct GamePortList {
|
|
const char *gameId[10];
|
|
uint16_t udpPorts[10];
|
|
uint16_t tcpPorts[10];
|
|
};
|
|
static GamePortList GamesPorts[] = {
|
|
{ // Alien Front Online
|
|
{ "MK-51171" },
|
|
{ 7980 },
|
|
{ },
|
|
},
|
|
{ // ChuChu Rocket
|
|
{ "MK-51049", "HDR-0039", "MK-5104950" },
|
|
{ 9789 },
|
|
{ },
|
|
},
|
|
{
|
|
{
|
|
"MK-51037", "HDR-0106", // Daytona USA
|
|
"HDR-0073", // Sega Tetris
|
|
"GENERIC", "T44501M" // Golf Shiyouyo 2
|
|
// (the dreamcastlive patched versions are id'ed as GENERIC)
|
|
},
|
|
{ 12079, 20675 },
|
|
{ },
|
|
},
|
|
{ // Dee Dee Planet
|
|
{ "HDR-0041" },
|
|
{ 9879 },
|
|
{ },
|
|
},
|
|
{ // Driving Strikers online demo
|
|
{ "IND-161053" },
|
|
{ 30099 },
|
|
{ },
|
|
},
|
|
{ // Floigan Bros
|
|
{ "MK-51114" },
|
|
{ },
|
|
{ 37001 },
|
|
},
|
|
{ // Internet Game Pack
|
|
{ "MK-51138" },
|
|
{ 5656 },
|
|
{ 5011, 10500, 10501, 10502, 10503 },
|
|
},
|
|
{ // NBA 2K1,2K2 / NFL 2K1,2K2 / NCAA 2K2
|
|
{ "MK-51063", "HDR-0150", // NBA 2K1
|
|
"MK-51178", "HDR-0197", "MK-5117850", // NBA 2K2
|
|
"MK-51062", "HDR-0144", // NFL 2K1
|
|
"MK-51168", "HDR-0196", // NFL 2K2
|
|
"MK-51176" }, // NCAA 2K2
|
|
{ 5502, 5503, 5656 },
|
|
{ 5011, 6666 },
|
|
},
|
|
{ // The Next Tetris
|
|
{ "T40214N", "T17717D 50" },
|
|
{ 3512 },
|
|
{ 3512 },
|
|
},
|
|
{ // Ooga Booga
|
|
{ "MK-51140" },
|
|
{ 6001 },
|
|
{ },
|
|
},
|
|
{ // PBA Tour Bowling 2001
|
|
{ "T26702N" },
|
|
{ 2300, 6500, 47624, 13139 }, // FIXME 2300-2400 ?
|
|
{ 2300, 47624 }, // FIXME 2300-2400 ?
|
|
},
|
|
{ // Planet Ring
|
|
{ "MK-5114864", "MK-5112550" },
|
|
{ 7648, 1285, 1028 },
|
|
{ },
|
|
},
|
|
{ // StarLancer
|
|
{ "T40209N", "T17723D 05" },
|
|
{ 2300, 6500, 47624 }, // FIXME 2300-2400 ?
|
|
{ 2300, 47624 }, // FIXME 2300-2400 ?
|
|
},
|
|
{ // World Series Baseball 2K2
|
|
{ "MK-51152", "HDR-0198" },
|
|
{ 37171, 13713 },
|
|
{ },
|
|
},
|
|
{ // Worms World Party
|
|
{ "T22904N", "T7016D 50" },
|
|
{ },
|
|
{ 17219 },
|
|
},
|
|
};
|
|
|
|
// listening port -> socket fd
|
|
static std::map<uint16_t, sock_t> tcp_listening_sockets;
|
|
|
|
static bool pico_stack_inited;
|
|
static retro_atomic_int_t pico_thread_running;
|
|
extern "C"
|
|
{
|
|
extern int dont_reject_opt_vj_hack;
|
|
};
|
|
|
|
static void read_native_sockets();
|
|
void get_host_by_name(const char *name, pico_ip4 dnsaddr);
|
|
int get_dns_answer(pico_ip4 *address, pico_ip4 dnsaddr);
|
|
sock_t get_dns_socket(void);
|
|
|
|
static int modem_read(pico_device *dev, void *data, int len)
|
|
{
|
|
int r = (int)retro_spsc_read(&out_ring, data, len);
|
|
if (r > 0)
|
|
ppp_passive = false;
|
|
return r;
|
|
}
|
|
|
|
static int modem_write(pico_device *dev, const void *data, int len)
|
|
{
|
|
const u8 *p = (const u8 *)data;
|
|
int left = len;
|
|
|
|
if (ppp_passive)
|
|
return len;
|
|
|
|
while (left > 0)
|
|
{
|
|
int key;
|
|
size_t n = retro_spsc_write(&in_ring, p, left);
|
|
|
|
p += n;
|
|
left -= (int)n;
|
|
if (left == 0)
|
|
break;
|
|
|
|
/* Full: sleep until the modem has read some of it. */
|
|
key = in_ring_ec.Prepare();
|
|
if (!retro_atomic_load_acquire_int(&pico_thread_running))
|
|
{
|
|
in_ring_ec.Cancel();
|
|
return 0;
|
|
}
|
|
if (retro_spsc_write_avail(&in_ring) > 0)
|
|
in_ring_ec.Cancel();
|
|
else
|
|
in_ring_ec.Commit(key, 5000);
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
void write_pico(u8 b)
|
|
{
|
|
if (rings_ready)
|
|
{
|
|
retro_spsc_write(&out_ring, &b, 1);
|
|
wake_pico();
|
|
}
|
|
}
|
|
int read_pico()
|
|
{
|
|
u8 b;
|
|
|
|
if (!rings_ready || retro_spsc_read(&in_ring, &b, 1) != 1)
|
|
return -1;
|
|
in_ring_ec.Notify();
|
|
if (retro_atomic_load_acquire_int(&in_ring_blocked))
|
|
{
|
|
retro_atomic_store_release_int(&in_ring_blocked, 0);
|
|
wake_pico();
|
|
}
|
|
return b;
|
|
}
|
|
|
|
static void read_from_dc_socket(socket_pair& pair)
|
|
{
|
|
char buf[1510];
|
|
int r;
|
|
|
|
/* When the Dreamcast closes with bytes still unread, the stack says so
|
|
* from inside the read that comes next, whatever that read leaves
|
|
* behind. The close is not passed on from there (see tcp_callback): it
|
|
* would go out ahead of the bytes this read returns, and of the ones
|
|
* after them. So once a close is owed, all there is gets read here. */
|
|
pair.reading = true;
|
|
do
|
|
{
|
|
r = pico_socket_read(pair.pico_sock, buf, sizeof(buf));
|
|
if (r > 0 && !pair.send_native(buf, r))
|
|
{
|
|
pico_socket *pico_sock = pair.pico_sock;
|
|
|
|
perror("tcp_callback send");
|
|
tcp_sockets.erase(pico_sock);
|
|
return;
|
|
}
|
|
/* All there is: a read event can stand for more than one read's
|
|
* worth, and no other comes until more arrives (what the native
|
|
* socket does not take is kept: see send_native()) */
|
|
} while (r > 0);
|
|
pair.reading = false;
|
|
pair.pass_close();
|
|
}
|
|
|
|
static void tcp_callback(uint16_t ev, pico_socket *s)
|
|
{
|
|
if (ev & PICO_SOCK_EV_RD)
|
|
{
|
|
auto it = tcp_sockets.find(s);
|
|
if (it == tcp_sockets.end())
|
|
{
|
|
if (tcp_connecting_sockets.find(s) == tcp_connecting_sockets.end())
|
|
INFO_LOG(MODEM, "Unknown socket: remote port %d", short_be(s->remote_port));
|
|
}
|
|
else
|
|
{
|
|
read_from_dc_socket(it->second);
|
|
}
|
|
}
|
|
|
|
if (ev & PICO_SOCK_EV_CONN)
|
|
{
|
|
pico_ip4 orig;
|
|
uint16_t port;
|
|
char peer[30];
|
|
int yes = 1;
|
|
|
|
pico_socket *sock_a = pico_socket_accept(s, &orig, &port);
|
|
if (sock_a == NULL)
|
|
{
|
|
// Also called for child sockets
|
|
if (tcp_sockets.find(s) == tcp_sockets.end())
|
|
INFO_LOG(MODEM, "pico_socket_accept: %s\n", strerror(pico_err));
|
|
}
|
|
else
|
|
{
|
|
pico_ipv4_to_string(peer, sock_a->local_addr.ip4.addr);
|
|
//printf("Connection established from port %d to %s:%d\n", short_be(port), peer, short_be(sock_a->local_port));
|
|
pico_socket_setoption(sock_a, PICO_TCP_NODELAY, &yes);
|
|
pico_tcp_set_linger(sock_a, 10000);
|
|
/* Set keepalive options */
|
|
// uint32_t ka_val = 5;
|
|
// pico_socket_setoption(sock_a, PICO_SOCKET_OPT_KEEPCNT, &ka_val);
|
|
// ka_val = 30000;
|
|
// pico_socket_setoption(sock_a, PICO_SOCKET_OPT_KEEPIDLE, &ka_val);
|
|
// ka_val = 5000;
|
|
// pico_socket_setoption(sock_a, PICO_SOCKET_OPT_KEEPINTVL, &ka_val);
|
|
|
|
sock_t sockfd = selectable_socket(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
|
|
if (!VALID(sockfd))
|
|
{
|
|
perror("socket");
|
|
}
|
|
else
|
|
{
|
|
sockaddr_in serveraddr;
|
|
memset(&serveraddr, 0, sizeof(serveraddr));
|
|
serveraddr.sin_family = AF_INET;
|
|
serveraddr.sin_addr.s_addr = sock_a->local_addr.ip4.addr;
|
|
if (serveraddr.sin_addr.s_addr == AFO_ORIG_IP // Alien Front Online
|
|
|| serveraddr.sin_addr.s_addr == IGP_ORIG_IP) // Internet Game Pack
|
|
{
|
|
serveraddr.sin_addr.s_addr = afo_ip.addr; // same ip for both for now
|
|
}
|
|
|
|
serveraddr.sin_port = sock_a->local_port;
|
|
set_non_blocking(sockfd);
|
|
set_no_sigpipe(sockfd);
|
|
if (connect(sockfd, (sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
|
|
{
|
|
if (get_last_error() != EINPROGRESS && get_last_error() != L_EWOULDBLOCK)
|
|
{
|
|
pico_ipv4_to_string(peer, sock_a->local_addr.ip4.addr);
|
|
INFO_LOG(MODEM, "TCP connection to %s:%d failed: %s", peer, short_be(sock_a->local_port), strerror(get_last_error()));
|
|
closesocket(sockfd);
|
|
}
|
|
else
|
|
tcp_connecting_sockets[sock_a] = sockfd;
|
|
}
|
|
else
|
|
{
|
|
set_tcp_nodelay(sockfd);
|
|
|
|
tcp_sockets.emplace(std::piecewise_construct,
|
|
std::forward_as_tuple(sock_a),
|
|
std::forward_as_tuple(sock_a, sockfd));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ev & PICO_SOCK_EV_FIN) {
|
|
auto it = tcp_sockets.find(s);
|
|
if (it != tcp_sockets.end())
|
|
{
|
|
tcp_sockets.erase(it);
|
|
}
|
|
else
|
|
{
|
|
auto it2 = tcp_connecting_sockets.find(s);
|
|
if (it2 != tcp_connecting_sockets.end())
|
|
{
|
|
closesocket(it2->second);
|
|
tcp_connecting_sockets.erase(it2);
|
|
}
|
|
else
|
|
INFO_LOG(MODEM, "PICO_SOCK_EV_FIN: Unknown socket: remote port %d", short_be(s->remote_port));
|
|
}
|
|
}
|
|
|
|
if (ev & PICO_SOCK_EV_ERR) {
|
|
INFO_LOG(MODEM, "Socket error received: %s", strerror(pico_err));
|
|
auto it = tcp_sockets.find(s);
|
|
if (it == tcp_sockets.end())
|
|
INFO_LOG(MODEM, "PICO_SOCK_EV_ERR: Unknown socket: remote port %d", short_be(s->remote_port));
|
|
else
|
|
tcp_sockets.erase(it);
|
|
}
|
|
|
|
if (ev & PICO_SOCK_EV_CLOSE)
|
|
{
|
|
auto it = tcp_sockets.find(s);
|
|
if (it == tcp_sockets.end())
|
|
{
|
|
INFO_LOG(MODEM, "PICO_SOCK_EV_CLOSE: Unknown socket: remote port %d", short_be(s->remote_port));
|
|
}
|
|
else
|
|
{
|
|
// Not before the bytes still waiting for the native socket have
|
|
// gone, nor those of a read this comes from the middle of
|
|
if (!it->second.out_buffer.empty() || it->second.reading)
|
|
it->second.shut_wr = true;
|
|
else if (it->second.native_sock != INVALID_SOCKET)
|
|
shutdown(it->second.native_sock, SHUT_WR);
|
|
pico_socket_shutdown(s, PICO_SHUT_RD);
|
|
}
|
|
}
|
|
|
|
// if (ev & PICO_SOCK_EV_WR)
|
|
// {
|
|
// }
|
|
}
|
|
|
|
static sock_t find_udp_socket(uint16_t src_port)
|
|
{
|
|
auto it = udp_sockets.find(src_port);
|
|
if (it != udp_sockets.end())
|
|
return it->second;
|
|
|
|
sock_t sockfd = selectable_socket(socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP));
|
|
if (!VALID(sockfd))
|
|
{
|
|
perror("socket");
|
|
return -1;
|
|
}
|
|
#ifndef _WIN32
|
|
fcntl(sockfd, F_SETFL, O_NONBLOCK);
|
|
#else
|
|
u_long optl = 1;
|
|
ioctlsocket(sockfd, FIONBIO, &optl);
|
|
#endif
|
|
|
|
// The port the Dreamcast sends from, when it can be had: a server may
|
|
// answer to the port the game is known to use and not to the one the
|
|
// datagram came from (Toy Racer). Otherwise any port the system has.
|
|
sockaddr_in saddr;
|
|
memset(&saddr, 0, sizeof(saddr));
|
|
saddr.sin_family = AF_INET;
|
|
saddr.sin_addr.s_addr = INADDR_ANY;
|
|
saddr.sin_port = src_port;
|
|
if (::bind(sockfd, (sockaddr *)&saddr, sizeof(saddr)) < 0)
|
|
{
|
|
INFO_LOG(MODEM, "UDP port %d cannot be had: using another", short_be(src_port));
|
|
saddr.sin_port = 0;
|
|
if (::bind(sockfd, (sockaddr *)&saddr, sizeof(saddr)) < 0)
|
|
perror("bind");
|
|
}
|
|
|
|
// FIXME Need to clean up at some point?
|
|
udp_sockets[src_port] = sockfd;
|
|
|
|
return sockfd;
|
|
}
|
|
|
|
static void udp_callback(uint16_t ev, pico_socket *s)
|
|
{
|
|
if (ev & PICO_SOCK_EV_RD)
|
|
{
|
|
char buf[1510];
|
|
pico_ip4 src_addr;
|
|
uint16_t src_port;
|
|
pico_msginfo msginfo;
|
|
int r = 0;
|
|
while (true)
|
|
{
|
|
r = pico_socket_recvfrom_extended(s, buf, sizeof(buf), &src_addr.addr, &src_port, &msginfo);
|
|
|
|
if (r <= 0)
|
|
{
|
|
if (r < 0)
|
|
INFO_LOG(MODEM, "error UDP recv: %s", strerror(pico_err));
|
|
break;
|
|
}
|
|
DEBUG_LOG(MODEM, "udp_callback(read) recvd port %d: %d bytes", short_be(src_port), r);
|
|
sock_t sockfd = find_udp_socket(src_port);
|
|
if (VALID(sockfd))
|
|
{
|
|
sockaddr_in dst_addr;
|
|
socklen_t addr_len = sizeof(dst_addr);
|
|
memset(&dst_addr, 0, sizeof(dst_addr));
|
|
dst_addr.sin_family = AF_INET;
|
|
dst_addr.sin_addr.s_addr = msginfo.local_addr.ip4.addr;
|
|
dst_addr.sin_port = msginfo.local_port;
|
|
if (sendto(sockfd, buf, r, L_MSG_NOSIGNAL, (const sockaddr *)&dst_addr, addr_len) < 0)
|
|
perror("sendto udp socket");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ev & PICO_SOCK_EV_ERR) {
|
|
INFO_LOG(MODEM, "UDP Callback error received");
|
|
}
|
|
}
|
|
|
|
static void read_native_sockets()
|
|
{
|
|
int r;
|
|
sockaddr_in src_addr;
|
|
socklen_t addr_len;
|
|
|
|
// Accept incoming TCP connections
|
|
for (auto it = tcp_listening_sockets.begin(); it != tcp_listening_sockets.end(); it++)
|
|
{
|
|
addr_len = sizeof(src_addr);
|
|
memset(&src_addr, 0, addr_len);
|
|
sock_t sockfd = selectable_socket(accept(it->second, (sockaddr *)&src_addr, &addr_len));
|
|
if (!VALID(sockfd))
|
|
{
|
|
if (get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK)
|
|
perror("accept");
|
|
continue;
|
|
}
|
|
//printf("Incoming TCP connection from %08x to port %d\n", src_addr.sin_addr.s_addr, short_be(it->first));
|
|
pico_socket *ps = pico_socket_open(PICO_PROTO_IPV4, PICO_PROTO_TCP, &tcp_callback);
|
|
if (ps == NULL)
|
|
{
|
|
INFO_LOG(MODEM, "pico_socket_open failed: error %d", pico_err);
|
|
closesocket(sockfd);
|
|
continue;
|
|
}
|
|
ps->local_addr.ip4.addr = src_addr.sin_addr.s_addr;
|
|
ps->local_port = src_addr.sin_port;
|
|
if (pico_socket_connect(ps, &dcaddr.addr, it->first) != 0)
|
|
{
|
|
INFO_LOG(MODEM, "pico_socket_connect failed: error %d", pico_err);
|
|
closesocket(sockfd);
|
|
pico_socket_close(ps);
|
|
continue;
|
|
}
|
|
set_non_blocking(sockfd);
|
|
set_tcp_nodelay(sockfd);
|
|
set_no_sigpipe(sockfd);
|
|
|
|
tcp_sockets.emplace(std::piecewise_construct,
|
|
std::forward_as_tuple(ps),
|
|
std::forward_as_tuple(ps, sockfd));
|
|
|
|
}
|
|
|
|
// Check connecting outbound TCP sockets
|
|
fd_set write_fds;
|
|
FD_ZERO(&write_fds);
|
|
int max_fd = -1;
|
|
for (auto it = tcp_connecting_sockets.begin(); it != tcp_connecting_sockets.end(); it++)
|
|
{
|
|
FD_SET(it->second, &write_fds);
|
|
max_fd = std::max(max_fd, (int)it->second);
|
|
}
|
|
timeval tv;
|
|
tv.tv_sec = 0;
|
|
tv.tv_usec = 0;
|
|
if (select(max_fd + 1, NULL, &write_fds, NULL, &tv) > 0)
|
|
{
|
|
for (auto it = tcp_connecting_sockets.begin(); it != tcp_connecting_sockets.end(); )
|
|
{
|
|
if (!FD_ISSET(it->second, &write_fds))
|
|
{
|
|
it++;
|
|
continue;
|
|
}
|
|
#ifdef _WIN32
|
|
char value;
|
|
#else
|
|
int value;
|
|
#endif
|
|
socklen_t l = sizeof(int);
|
|
if (getsockopt(it->second, SOL_SOCKET, SO_ERROR, &value, &l) < 0 || value)
|
|
{
|
|
char peer[30];
|
|
pico_ipv4_to_string(peer, it->first->local_addr.ip4.addr);
|
|
INFO_LOG(MODEM, "TCP connection to %s:%d failed: %s", peer, short_be(it->first->local_port), strerror(get_last_error()));
|
|
pico_socket_close(it->first);
|
|
closesocket(it->second);
|
|
}
|
|
else
|
|
{
|
|
set_tcp_nodelay(it->second);
|
|
|
|
auto added = tcp_sockets.emplace(std::piecewise_construct,
|
|
std::forward_as_tuple(it->first),
|
|
std::forward_as_tuple(it->first, it->second));
|
|
|
|
read_from_dc_socket(added.first->second);
|
|
}
|
|
it = tcp_connecting_sockets.erase(it);
|
|
}
|
|
}
|
|
|
|
char buf[1500];
|
|
pico_msginfo msginfo;
|
|
|
|
// Read UDP sockets
|
|
for (auto it = udp_sockets.begin(); it != udp_sockets.end(); it++)
|
|
{
|
|
if (!VALID(it->second))
|
|
continue;
|
|
|
|
addr_len = sizeof(src_addr);
|
|
memset(&src_addr, 0, addr_len);
|
|
r = (int)recvfrom(it->second, buf, sizeof(buf), 0, (sockaddr *)&src_addr, &addr_len);
|
|
if (r > 0)
|
|
{
|
|
msginfo.dev = pico_dev;
|
|
msginfo.tos = 0;
|
|
msginfo.ttl = 0;
|
|
msginfo.local_addr.ip4.addr = src_addr.sin_addr.s_addr;
|
|
msginfo.local_port = src_addr.sin_port;
|
|
DEBUG_LOG(MODEM, "read_native_sockets UDP received %d bytes from %08x:%d", r, long_be(msginfo.local_addr.ip4.addr), short_be(msginfo.local_port));
|
|
int r2 = pico_socket_sendto_extended(pico_udp_socket, buf, r, &dcaddr, it->first, &msginfo);
|
|
if (r2 < r)
|
|
INFO_LOG(MODEM, "error UDP sending to %d: %s", short_be(it->first), strerror(pico_err));
|
|
}
|
|
else if (r < 0 && get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK)
|
|
{
|
|
perror("recvfrom udp socket");
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Read TCP sockets
|
|
for (auto it = tcp_sockets.begin(); it != tcp_sockets.end(); )
|
|
{
|
|
if (!it->second.out_buffer.empty() && VALID(it->second.native_sock)
|
|
&& !it->second.send_native(nullptr, 0))
|
|
{
|
|
perror("tcp socket send");
|
|
it = tcp_sockets.erase(it);
|
|
continue;
|
|
}
|
|
it->second.receive_native();
|
|
if (it->second.pico_sock == nullptr)
|
|
it = tcp_sockets.erase(it);
|
|
else
|
|
it++;
|
|
}
|
|
}
|
|
|
|
static void close_native_sockets()
|
|
{
|
|
for (auto it = udp_sockets.begin(); it != udp_sockets.end(); it++)
|
|
closesocket(it->second);
|
|
udp_sockets.clear();
|
|
tcp_sockets.clear();
|
|
for (auto it = tcp_connecting_sockets.begin(); it != tcp_connecting_sockets.end(); it++)
|
|
{
|
|
pico_socket_close(it->first);
|
|
closesocket(it->second);
|
|
}
|
|
tcp_connecting_sockets.clear();
|
|
}
|
|
|
|
static int modem_set_speed(pico_device *dev, uint32_t speed)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static void check_dns_entries()
|
|
{
|
|
static uint32_t dns_query_start = 0;
|
|
static uint32_t dns_query_attempts = 0;
|
|
|
|
if (afo_ip.addr == 0)
|
|
{
|
|
if (!dns_query_start)
|
|
{
|
|
dns_query_start = PICO_TIME_MS();
|
|
get_host_by_name("auriga.segasoft.com", dnsaddr); // Alien Front Online server
|
|
}
|
|
else
|
|
{
|
|
if (get_dns_answer(&afo_ip, dnsaddr) == 0)
|
|
{
|
|
dns_query_attempts = 0;
|
|
dns_query_start = 0;
|
|
char afoip[16];
|
|
pico_ipv4_to_string(afoip, afo_ip.addr);
|
|
INFO_LOG(MODEM, "AFO server IP is %s", afoip);
|
|
}
|
|
else
|
|
{
|
|
if (PICO_TIME_MS() - dns_query_start > 1000)
|
|
{
|
|
if (++dns_query_attempts >= 5)
|
|
{
|
|
u32 addr;
|
|
pico_string_to_ipv4("146.185.135.179", &addr); // Default address
|
|
memcpy(&afo_ip.addr, &addr, sizeof(addr));
|
|
dns_query_attempts = 0;
|
|
}
|
|
else
|
|
// Retry
|
|
dns_query_start = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static pico_device *pico_eth_create()
|
|
{
|
|
pico_device *eth = (pico_device *)PICO_ZALLOC(sizeof(pico_device));
|
|
if (!eth)
|
|
return nullptr;
|
|
|
|
const u8 mac_addr[6] = { 0xc, 0xa, 0xf, 0xe, 0, 1 };
|
|
if (0 != pico_device_init(eth, "ETHPEER", mac_addr))
|
|
return nullptr;
|
|
|
|
DEBUG_LOG(NETWORK, "Device %s created", eth->name);
|
|
|
|
return eth;
|
|
}
|
|
|
|
// A capture of every frame of the adapter, for debugging: never in a release
|
|
//#define BBA_PCAPNG_DUMP
|
|
static FILE *pcapngDump;
|
|
|
|
static void dumpFrame(const u8 *frame, u32 size)
|
|
{
|
|
#ifdef BBA_PCAPNG_DUMP
|
|
if (pcapngDump == nullptr)
|
|
{
|
|
const char *home = getenv("HOME");
|
|
if (home == nullptr)
|
|
return;
|
|
std::string path = home + std::string("/bba.pcapng");
|
|
pcapngDump = fopen(path.c_str(), "wb");
|
|
if (pcapngDump == nullptr)
|
|
return;
|
|
u32 blockType = 0x0A0D0D0A; // Section Header Block
|
|
fwrite(&blockType, sizeof(blockType), 1, pcapngDump);
|
|
u32 blockLen = 28;
|
|
fwrite(&blockLen, sizeof(blockLen), 1, pcapngDump);
|
|
u32 magic = 0x1A2B3C4D;
|
|
fwrite(&magic, sizeof(magic), 1, pcapngDump);
|
|
u32 version = 1; // 1.0
|
|
fwrite(&version, sizeof(version), 1, pcapngDump);
|
|
u64 sectionLength = ~0; // unspecified
|
|
fwrite(§ionLength, sizeof(sectionLength), 1, pcapngDump);
|
|
fwrite(&blockLen, sizeof(blockLen), 1, pcapngDump);
|
|
|
|
blockType = 1; // Interface Description Block
|
|
fwrite(&blockType, sizeof(blockType), 1, pcapngDump);
|
|
blockLen = 20;
|
|
fwrite(&blockLen, sizeof(blockLen), 1, pcapngDump);
|
|
const u32 linkType = 1; // Ethernet
|
|
fwrite(&linkType, sizeof(linkType), 1, pcapngDump);
|
|
const u32 snapLen = 0; // no limit
|
|
fwrite(&snapLen, sizeof(snapLen), 1, pcapngDump);
|
|
// TODO options? if name, ip/mac address
|
|
fwrite(&blockLen, sizeof(blockLen), 1, pcapngDump);
|
|
}
|
|
const u32 blockType = 3; // Simple Packet Block
|
|
fwrite(&blockType, sizeof(blockType), 1, pcapngDump);
|
|
u32 roundedSize = ((size + 3) & ~3) + 16;
|
|
fwrite(&roundedSize, sizeof(roundedSize), 1, pcapngDump);
|
|
fwrite(&size, sizeof(size), 1, pcapngDump);
|
|
fwrite(frame, 1, size, pcapngDump);
|
|
fwrite(frame, 1, roundedSize - size - 16, pcapngDump);
|
|
fwrite(&roundedSize, sizeof(roundedSize), 1, pcapngDump);
|
|
#endif
|
|
}
|
|
static void closeDumpFile()
|
|
{
|
|
if (pcapngDump != nullptr)
|
|
{
|
|
fclose(pcapngDump);
|
|
pcapngDump = nullptr;
|
|
}
|
|
}
|
|
/* Emulation thread: the adapter has sent a frame. */
|
|
void pico_receive_eth_frame(const u8 *frame, u32 size)
|
|
{
|
|
u8 buf[2 + ETH_FRAME_MAX];
|
|
|
|
if (!rings_ready || size == 0 || size > ETH_FRAME_MAX)
|
|
return;
|
|
// No room: dropped, as a switch with a full queue would
|
|
if (retro_spsc_write_avail(ð_out_ring) < size + 2)
|
|
return;
|
|
buf[0] = (u8)size;
|
|
buf[1] = (u8)(size >> 8);
|
|
memcpy(&buf[2], frame, size);
|
|
retro_spsc_write(ð_out_ring, buf, size + 2);
|
|
wake_pico();
|
|
}
|
|
|
|
/* Pico thread: give the stack the frames the adapter has sent. */
|
|
/* No more than this many in one go. A guest that sends without stopping
|
|
* refills the ring as fast as it is emptied: taken until the ring was
|
|
* empty, its frames kept this thread here for good, and the stack's
|
|
* timers and the sockets - which come after - never had their turn (an
|
|
* ARP request in such a flood got its first answer and no other). */
|
|
#define ETH_FRAMES_A_PASS 256
|
|
|
|
/* True if there may be more waiting: the caller comes back for them once
|
|
* the rest has had its turn, without sleeping first. */
|
|
static bool take_eth_frames()
|
|
{
|
|
u8 buf[ETH_FRAME_MAX];
|
|
u8 len[2];
|
|
|
|
for (int taken = 0; taken < ETH_FRAMES_A_PASS; taken++)
|
|
{
|
|
if (retro_spsc_read(ð_out_ring, len, 2) != 2)
|
|
return false;
|
|
|
|
const u32 size = len[0] | (len[1] << 8);
|
|
|
|
if (size > ETH_FRAME_MAX || retro_spsc_read(ð_out_ring, buf, size) != size)
|
|
return false;
|
|
dumpFrame(buf, size);
|
|
if (pico_dev != nullptr)
|
|
pico_stack_recv(pico_dev, buf, size);
|
|
}
|
|
return true;
|
|
}
|
|
/* Pico thread: the stack has a frame for the adapter. */
|
|
static int send_eth_frame(pico_device *dev, void *data, int len)
|
|
{
|
|
u8 buf[2 + ETH_FRAME_MAX];
|
|
|
|
if (len <= 0 || len > ETH_FRAME_MAX)
|
|
return len; // nothing the adapter could take: let the stack drop it
|
|
if (retro_spsc_write_avail(ð_in_ring) < (size_t)len + 2)
|
|
{
|
|
// The stack keeps the frame and tries again when it is next woken
|
|
retro_atomic_store_release_int(ð_in_blocked, 1);
|
|
if (retro_spsc_write_avail(ð_in_ring) < (size_t)len + 2)
|
|
return 0;
|
|
}
|
|
dumpFrame((const u8 *)data, len);
|
|
buf[0] = (u8)len;
|
|
buf[1] = (u8)(len >> 8);
|
|
memcpy(&buf[2], data, len);
|
|
retro_spsc_write(ð_in_ring, buf, len + 2);
|
|
return len;
|
|
}
|
|
|
|
/* Emulation thread: the next frame the stack has sent the adapter, into
|
|
* @frame (ETH_FRAME_MAX bytes). Its size, 0 if there is none. */
|
|
u32 net_eth_take(u8 *frame)
|
|
{
|
|
u8 len[2];
|
|
|
|
if (!rings_ready || retro_spsc_read(ð_in_ring, len, 2) != 2)
|
|
return 0;
|
|
u32 size = len[0] | (len[1] << 8);
|
|
// (frames are written whole: this cannot be short)
|
|
if (size > ETH_FRAME_MAX || retro_spsc_read(ð_in_ring, frame, size) != size)
|
|
size = 0;
|
|
if (retro_atomic_load_acquire_int(ð_in_blocked))
|
|
{
|
|
retro_atomic_store_release_int(ð_in_blocked, 0);
|
|
wake_pico();
|
|
}
|
|
return size;
|
|
}
|
|
|
|
/* Waits until the thread has something to do: a native socket to read,
|
|
* accept on or finish connecting, a timer of the stack's come due,
|
|
* something handed over by the emulation thread, or the order to stop.
|
|
* @kicks is pico_kicks as it was before the thread last looked for work. */
|
|
static void wait_for_work(int kicks)
|
|
{
|
|
fd_set readable, writable;
|
|
timeval tv;
|
|
char drain[64];
|
|
int max_fd = (int)wake_sock;
|
|
int idle = pico_stack_idle_ms();
|
|
|
|
// The last tick moved a frame: there may be more
|
|
if (idle == 0)
|
|
return;
|
|
// Never longer than a second, whatever the stack's timers say
|
|
if (idle < 0 || idle > 1000)
|
|
idle = 1000;
|
|
|
|
FD_ZERO(&readable);
|
|
FD_ZERO(&writable);
|
|
FD_SET(wake_sock, &readable);
|
|
for (auto& it : udp_sockets)
|
|
if (VALID(it.second))
|
|
{
|
|
FD_SET(it.second, &readable);
|
|
max_fd = std::max(max_fd, (int)it.second);
|
|
}
|
|
for (auto& it : tcp_listening_sockets)
|
|
{
|
|
FD_SET(it.second, &readable);
|
|
max_fd = std::max(max_fd, (int)it.second);
|
|
}
|
|
for (auto& it : tcp_sockets)
|
|
{
|
|
// One with bytes the stack has not taken yet is not read until it has
|
|
if (VALID(it.second.native_sock) && it.second.in_buffer.empty())
|
|
{
|
|
FD_SET(it.second.native_sock, &readable);
|
|
max_fd = std::max(max_fd, (int)it.second.native_sock);
|
|
}
|
|
// One that is owed bytes: woken when it has room for them
|
|
if (VALID(it.second.native_sock) && !it.second.out_buffer.empty())
|
|
{
|
|
FD_SET(it.second.native_sock, &writable);
|
|
max_fd = std::max(max_fd, (int)it.second.native_sock);
|
|
}
|
|
}
|
|
for (auto& it : tcp_connecting_sockets)
|
|
{
|
|
FD_SET(it.second, &writable);
|
|
max_fd = std::max(max_fd, (int)it.second);
|
|
}
|
|
if (afo_ip.addr == 0 && VALID(get_dns_socket()))
|
|
{
|
|
FD_SET(get_dns_socket(), &readable);
|
|
max_fd = std::max(max_fd, (int)get_dns_socket());
|
|
}
|
|
|
|
tv.tv_sec = idle / 1000;
|
|
tv.tv_usec = (idle % 1000) * 1000;
|
|
|
|
retro_atomic_fetch_add_int(&pico_waiting, 1);
|
|
if (retro_atomic_load_acquire_int(&pico_thread_running)
|
|
&& retro_atomic_load_acquire_int(&pico_kicks) == kicks)
|
|
select(max_fd + 1, &readable, &writable, NULL, &tv);
|
|
retro_atomic_store_release_int(&pico_waiting, 0);
|
|
|
|
while (recv(wake_sock, drain, sizeof(drain), 0) > 0)
|
|
;
|
|
}
|
|
|
|
/* For DCNet (dcnet.cpp), which runs on this thread in place of the stack:
|
|
* the same rings, the same way of waiting. */
|
|
bool netthread_running()
|
|
{
|
|
return retro_atomic_load_acquire_int(&pico_thread_running) != 0;
|
|
}
|
|
|
|
int netthread_kicks()
|
|
{
|
|
return retro_atomic_load_acquire_int(&pico_kicks);
|
|
}
|
|
|
|
size_t netthread_modem_take(u8 *buf, size_t len)
|
|
{
|
|
return retro_spsc_read(&out_ring, buf, len);
|
|
}
|
|
|
|
size_t netthread_modem_give(const u8 *buf, size_t len)
|
|
{
|
|
size_t n = retro_spsc_write(&in_ring, buf, len);
|
|
if (n < len)
|
|
{
|
|
// Full: the modem wakes the thread when it has read some (read_pico())
|
|
retro_atomic_store_release_int(&in_ring_blocked, 1);
|
|
n += retro_spsc_write(&in_ring, buf + n, len - n);
|
|
}
|
|
return n;
|
|
}
|
|
|
|
u32 netthread_eth_take(u8 *frame)
|
|
{
|
|
u8 len[2];
|
|
|
|
if (retro_spsc_read(ð_out_ring, len, 2) != 2)
|
|
return 0;
|
|
const u32 size = len[0] | (len[1] << 8);
|
|
if (size > ETH_FRAME_MAX || retro_spsc_read(ð_out_ring, frame, size) != size)
|
|
return 0;
|
|
return size;
|
|
}
|
|
|
|
bool netthread_eth_give(const u8 *frame, u32 size)
|
|
{
|
|
return send_eth_frame(nullptr, (void *)frame, (int)size) != 0;
|
|
}
|
|
|
|
void netthread_wait(int kicks, fd_set *readable, fd_set *writable, int max_fd, int timeout_ms)
|
|
{
|
|
timeval tv;
|
|
char drain[64];
|
|
|
|
FD_SET(wake_sock, readable);
|
|
max_fd = std::max(max_fd, (int)wake_sock);
|
|
tv.tv_sec = timeout_ms / 1000;
|
|
tv.tv_usec = (timeout_ms % 1000) * 1000;
|
|
retro_atomic_fetch_add_int(&pico_waiting, 1);
|
|
if (retro_atomic_load_acquire_int(&pico_thread_running)
|
|
&& retro_atomic_load_acquire_int(&pico_kicks) == kicks)
|
|
select(max_fd + 1, readable, writable, NULL, &tv);
|
|
retro_atomic_store_release_int(&pico_waiting, 0);
|
|
|
|
while (recv(wake_sock, drain, sizeof(drain), 0) > 0)
|
|
;
|
|
}
|
|
|
|
/* The IPv4 address of @host, 0 if it has none. (This can take seconds: the
|
|
* emulation thread never waits for the network thread but at its next
|
|
* start, see stop_pico().) */
|
|
u32 netthread_resolve(const char *host)
|
|
{
|
|
u32 ip = 0;
|
|
addrinfo hints;
|
|
addrinfo *res = nullptr;
|
|
|
|
memset(&hints, 0, sizeof(hints));
|
|
hints.ai_family = AF_INET;
|
|
if (getaddrinfo(host, nullptr, &hints, &res) == 0 && res != nullptr)
|
|
{
|
|
for (addrinfo *ai = res; ai != nullptr; ai = ai->ai_next)
|
|
if (ai->ai_family == AF_INET && ai->ai_addrlen >= sizeof(sockaddr_in))
|
|
{
|
|
ip = ((const sockaddr_in *)ai->ai_addr)->sin_addr.s_addr;
|
|
break;
|
|
}
|
|
freeaddrinfo(res);
|
|
}
|
|
return ip;
|
|
}
|
|
|
|
/* The Dreamcast's DNS server: the one asked for, the current default for
|
|
* the old one */
|
|
static void find_dns_server()
|
|
{
|
|
std::string name = settings.network.dns;
|
|
if (name == "46.101.91.123")
|
|
name = "dns.flyca.st";
|
|
u32 ip = 0;
|
|
if (pico_string_to_ipv4(name.c_str(), &ip) != 0 || ip == 0)
|
|
ip = netthread_resolve(name.c_str());
|
|
if (ip != 0)
|
|
{
|
|
char s[16];
|
|
memcpy(&dnsaddr.addr, &ip, sizeof(ip));
|
|
pico_ipv4_to_string(s, dnsaddr.addr);
|
|
NOTICE_LOG(MODEM, "%s IP is %s", name.c_str(), s);
|
|
}
|
|
else
|
|
{
|
|
pico_string_to_ipv4("46.101.91.123", &ip);
|
|
memcpy(&dnsaddr.addr, &ip, sizeof(ip));
|
|
WARN_LOG(MODEM, "Can't resolve %s. Using 46.101.91.123", name.c_str());
|
|
}
|
|
}
|
|
|
|
static void *pico_thread_func(void *)
|
|
{
|
|
if (thread_dcnet)
|
|
{
|
|
dcnet_run(thread_bba);
|
|
return NULL;
|
|
}
|
|
const bool bba = thread_bba;
|
|
// (the PPP stack's first words are to wait for the Dreamcast's)
|
|
ppp_passive = !bba;
|
|
if (!pico_stack_inited)
|
|
{
|
|
pico_stack_init();
|
|
pico_stack_inited = true;
|
|
#if _WIN32
|
|
static WSADATA wsaData;
|
|
if (WSAStartup(MAKEWORD(2, 0), &wsaData) != 0)
|
|
WARN_LOG(MODEM, "WSAStartup failed");
|
|
#endif
|
|
}
|
|
|
|
// Find the network ports for the current game
|
|
const GamePortList *ports = nullptr;
|
|
std::string gameId(ip_meta.product_number, sizeof(ip_meta.product_number));
|
|
gameId = trim_trailing_ws(gameId);
|
|
for (u32 i = 0; i < ARRAY_SIZE(GamesPorts) && ports == nullptr; i++)
|
|
{
|
|
const auto& game = GamesPorts[i];
|
|
for (u32 j = 0; j < ARRAY_SIZE(game.gameId) && game.gameId[j] != nullptr; j++)
|
|
{
|
|
if (gameId == game.gameId[j])
|
|
{
|
|
NOTICE_LOG(MODEM, "Found network ports for game %s", gameId.c_str());
|
|
ports = &game;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Web TV requires the VJ compression option, which picotcp doesn't support.
|
|
// This hack allows WebTV to connect although the correct fix would
|
|
// be to implement VJ compression.
|
|
dont_reject_opt_vj_hack = gameId == "6107117" ? 1 : 0;
|
|
|
|
// Initialize miniupnpc and map network ports
|
|
MiniUPnP upnp;
|
|
bool upnp_mapped = false;
|
|
if (ports != nullptr)
|
|
{
|
|
if (!upnp.Init())
|
|
WARN_LOG(MODEM, "UPNP Init failed");
|
|
else
|
|
{
|
|
upnp_mapped = true;
|
|
for (u32 i = 0; i < ARRAY_SIZE(ports->udpPorts) && ports->udpPorts[i] != 0; i++)
|
|
if (!upnp.AddPortMapping(ports->udpPorts[i], false))
|
|
WARN_LOG(MODEM, "UPNP AddPortMapping UDP %d failed", ports->udpPorts[i]);
|
|
for (u32 i = 0; i < ARRAY_SIZE(ports->tcpPorts) && ports->tcpPorts[i] != 0; i++)
|
|
if (!upnp.AddPortMapping(ports->tcpPorts[i], true))
|
|
WARN_LOG(MODEM, "UPNP AddPortMapping TCP %d failed", ports->tcpPorts[i]);
|
|
}
|
|
}
|
|
|
|
u32 addr;
|
|
find_dns_server();
|
|
|
|
// Create ppp/eth device
|
|
if (!bba)
|
|
{
|
|
// PPP
|
|
pico_dev = pico_ppp_create();
|
|
if (!pico_dev)
|
|
return NULL;
|
|
pico_string_to_ipv4("192.168.167.2", &addr);
|
|
memcpy(&dcaddr.addr, &addr, sizeof(addr));
|
|
pico_ppp_set_peer_ip(pico_dev, dcaddr);
|
|
pico_string_to_ipv4("192.168.167.1", &addr);
|
|
pico_ip4 ipaddr;
|
|
memcpy(&ipaddr.addr, &addr, sizeof(addr));
|
|
pico_ppp_set_ip(pico_dev, ipaddr);
|
|
pico_ppp_set_dns1(pico_dev, dnsaddr);
|
|
|
|
pico_ppp_set_serial_read(pico_dev, modem_read);
|
|
pico_ppp_set_serial_write(pico_dev, modem_write);
|
|
pico_ppp_set_serial_set_speed(pico_dev, modem_set_speed);
|
|
pico_dev->proxied = 1;
|
|
|
|
pico_ppp_connect(pico_dev);
|
|
}
|
|
else
|
|
{
|
|
// Ethernet
|
|
pico_dev = pico_eth_create();
|
|
if (pico_dev == nullptr)
|
|
return nullptr;
|
|
pico_dev->send = &send_eth_frame;
|
|
pico_dev->proxied = 1;
|
|
|
|
pico_string_to_ipv4("192.168.169.1", &addr);
|
|
pico_ip4 ipaddr;
|
|
memcpy(&ipaddr.addr, &addr, sizeof(addr));
|
|
pico_string_to_ipv4("255.255.255.0", &addr);
|
|
pico_ip4 netmask;
|
|
memcpy(&netmask.addr, &addr, sizeof(addr));
|
|
pico_ipv4_link_add(pico_dev, ipaddr, netmask);
|
|
// dreamcast IP
|
|
pico_string_to_ipv4("192.168.169.2", &addr);
|
|
memcpy(&dcaddr.addr, &addr, sizeof(addr));
|
|
|
|
pico_dhcp_server_setting dhcpSettings{ 0 };
|
|
dhcpSettings.dev = pico_dev;
|
|
dhcpSettings.server_ip = ipaddr;
|
|
dhcpSettings.lease_time = long_be(24 * 60 * 60); // seconds
|
|
dhcpSettings.netmask = netmask;
|
|
dhcpSettings.pool_start = addr;
|
|
dhcpSettings.pool_end = addr;
|
|
dhcpSettings.pool_next = addr;
|
|
dhcpSettings.dns_server.addr = dnsaddr.addr;
|
|
if (pico_dhcp_server_initiate(&dhcpSettings) != 0)
|
|
WARN_LOG(MODEM, "DHCP server init failed");
|
|
}
|
|
|
|
pico_udp_socket = pico_socket_open(PICO_PROTO_IPV4, PICO_PROTO_UDP, &udp_callback);
|
|
if (pico_udp_socket == NULL) {
|
|
INFO_LOG(MODEM, "error opening UDP socket: %s", strerror(pico_err));
|
|
return nullptr;
|
|
}
|
|
int yes = 1;
|
|
pico_ip4 inaddr_any = {0};
|
|
uint16_t listen_port = 0;
|
|
int ret = pico_socket_bind(pico_udp_socket, &inaddr_any, &listen_port);
|
|
if (ret < 0)
|
|
INFO_LOG(MODEM, "error binding UDP socket to port %u: %s", short_be(listen_port), strerror(pico_err));
|
|
|
|
pico_tcp_socket = pico_socket_open(PICO_PROTO_IPV4, PICO_PROTO_TCP, &tcp_callback);
|
|
if (pico_tcp_socket == NULL) {
|
|
INFO_LOG(MODEM, "error opening TCP socket: %s", strerror(pico_err));
|
|
}
|
|
pico_socket_setoption(pico_tcp_socket, PICO_TCP_NODELAY, &yes);
|
|
ret = pico_socket_bind(pico_tcp_socket, &inaddr_any, &listen_port);
|
|
if (ret < 0) {
|
|
INFO_LOG(MODEM, "error binding TCP socket to port %u: %s", short_be(listen_port), strerror(pico_err));
|
|
}
|
|
else
|
|
{
|
|
if (pico_socket_listen(pico_tcp_socket, 10) != 0)
|
|
INFO_LOG(MODEM, "error listening on port %u", short_be(listen_port));
|
|
}
|
|
|
|
// Open listening sockets
|
|
sockaddr_in saddr;
|
|
socklen_t saddr_len = sizeof(saddr);
|
|
memset(&saddr, 0, sizeof(saddr));
|
|
saddr.sin_family = AF_INET;
|
|
saddr.sin_addr.s_addr = INADDR_ANY;
|
|
if (ports != nullptr)
|
|
{
|
|
for (u32 i = 0; i < ARRAY_SIZE(ports->udpPorts) && ports->udpPorts[i] != 0; i++)
|
|
{
|
|
// Bound to the port by find_udp_socket
|
|
find_udp_socket(short_be(ports->udpPorts[i]));
|
|
}
|
|
|
|
for (u32 i = 0; i < ARRAY_SIZE(ports->tcpPorts) && ports->tcpPorts[i] != 0; i++)
|
|
{
|
|
uint16_t port = short_be(ports->tcpPorts[i]);
|
|
saddr.sin_port = port;
|
|
sock_t sockfd = selectable_socket(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
|
|
if (!VALID(sockfd))
|
|
{
|
|
perror("socket");
|
|
continue;
|
|
}
|
|
set_reuse_addr(sockfd);
|
|
if (::bind(sockfd, (sockaddr *)&saddr, saddr_len) < 0)
|
|
{
|
|
perror("bind");
|
|
closesocket(sockfd);
|
|
continue;
|
|
}
|
|
if (listen(sockfd, 5) < 0)
|
|
{
|
|
perror("listen");
|
|
closesocket(sockfd);
|
|
continue;
|
|
}
|
|
set_non_blocking(sockfd);
|
|
tcp_listening_sockets[port] = sockfd;
|
|
}
|
|
}
|
|
|
|
while (retro_atomic_load_acquire_int(&pico_thread_running))
|
|
{
|
|
const int kicks = retro_atomic_load_acquire_int(&pico_kicks);
|
|
|
|
const bool more_frames = take_eth_frames();
|
|
read_native_sockets();
|
|
pico_stack_tick();
|
|
check_dns_entries();
|
|
if (!more_frames)
|
|
wait_for_work(kicks);
|
|
}
|
|
for (auto it = tcp_listening_sockets.begin(); it != tcp_listening_sockets.end(); it++)
|
|
closesocket(it->second);
|
|
// The next connection must not wait on, or accept from, these numbers
|
|
tcp_listening_sockets.clear();
|
|
close_native_sockets();
|
|
pico_socket_close(pico_tcp_socket);
|
|
pico_socket_close(pico_udp_socket);
|
|
|
|
if (pico_dev)
|
|
{
|
|
if (!bba)
|
|
{
|
|
pico_ppp_destroy(pico_dev);
|
|
}
|
|
else
|
|
{
|
|
closeDumpFile();
|
|
pico_dhcp_server_destroy(pico_dev);
|
|
pico_device_destroy(pico_dev);
|
|
}
|
|
pico_dev = nullptr;
|
|
}
|
|
pico_stack_tick();
|
|
if (upnp_mapped)
|
|
upnp.Term();
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static cThread pico_thread(pico_thread_func, NULL);
|
|
|
|
static bool make_wake_socket()
|
|
{
|
|
sockaddr_in addr;
|
|
socklen_t len = sizeof(addr);
|
|
|
|
if (VALID(wake_sock))
|
|
return true;
|
|
#ifdef _WIN32
|
|
WSADATA wsaData;
|
|
if (WSAStartup(MAKEWORD(2, 0), &wsaData) != 0)
|
|
return false;
|
|
#endif
|
|
sock_t sock = selectable_socket(socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP));
|
|
if (!VALID(sock))
|
|
return false;
|
|
memset(&addr, 0, sizeof(addr));
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
if (::bind(sock, (sockaddr *)&addr, sizeof(addr)) < 0
|
|
|| getsockname(sock, (sockaddr *)&addr, &len) < 0
|
|
|| ::connect(sock, (sockaddr *)&addr, sizeof(addr)) < 0)
|
|
{
|
|
closesocket(sock);
|
|
return false;
|
|
}
|
|
set_non_blocking(sock);
|
|
wake_sock = sock;
|
|
return true;
|
|
}
|
|
|
|
bool start_pico()
|
|
{
|
|
if (retro_atomic_load_relaxed_int(&pico_thread_running))
|
|
return false;
|
|
// The last one, if it has not ended yet, is about to
|
|
pico_thread.WaitToEnd();
|
|
/* The pico thread is not running, so both rings are this thread's
|
|
* alone: make them, or empty them of the last connection's bytes. */
|
|
if (!rings_ready)
|
|
{
|
|
if (!retro_spsc_init(&in_ring, IN_RING_BYTES))
|
|
return false;
|
|
if (!retro_spsc_init(&out_ring, OUT_RING_BYTES))
|
|
{
|
|
retro_spsc_free(&in_ring);
|
|
return false;
|
|
}
|
|
if (!retro_spsc_init(ð_out_ring, ETH_RING_BYTES))
|
|
{
|
|
retro_spsc_free(&in_ring);
|
|
retro_spsc_free(&out_ring);
|
|
return false;
|
|
}
|
|
if (!retro_spsc_init(ð_in_ring, ETH_RING_BYTES))
|
|
{
|
|
retro_spsc_free(&in_ring);
|
|
retro_spsc_free(&out_ring);
|
|
retro_spsc_free(ð_out_ring);
|
|
return false;
|
|
}
|
|
rings_ready = true;
|
|
}
|
|
else
|
|
{
|
|
retro_spsc_clear(&in_ring);
|
|
retro_spsc_clear(&out_ring);
|
|
retro_spsc_clear(ð_out_ring);
|
|
retro_spsc_clear(ð_in_ring);
|
|
}
|
|
retro_atomic_store_release_int(ð_in_blocked, 0);
|
|
retro_atomic_store_release_int(&in_ring_blocked, 0);
|
|
if (!make_wake_socket())
|
|
return false;
|
|
thread_bba = net_device == NET_DEVICE_BBA;
|
|
{
|
|
std::string gameId(ip_meta.product_number, sizeof(ip_meta.product_number));
|
|
gameId = trim_trailing_ws(gameId);
|
|
thread_dcnet = net_dcnet_option
|
|
// these games' servers are reached through DCNet only
|
|
|| gameId == "HDR-0113" // Power Smash
|
|
|| gameId == "HDR-0091"; // Pro Yakyuu Team de Asobou Net!
|
|
if (thread_dcnet)
|
|
NOTICE_LOG(MODEM, "Network through DCNet");
|
|
}
|
|
retro_atomic_store_release_int(&pico_waiting, 0);
|
|
retro_atomic_store_release_int(&pico_thread_running, 1);
|
|
pico_thread.Start();
|
|
return true;
|
|
}
|
|
|
|
/* The thread is told to stop and is not waited for: it may be looking up a
|
|
* name or talking to the router (UPnP), which can take seconds. It is waited
|
|
* for before the next one starts, and when the machine stops. */
|
|
void stop_pico()
|
|
{
|
|
retro_atomic_store_release_int(&pico_thread_running, 0);
|
|
in_ring_ec.Notify();
|
|
if (VALID(wake_sock))
|
|
send(wake_sock, "", 1, L_MSG_NOSIGNAL);
|
|
}
|
|
|
|
void net_thread_join()
|
|
{
|
|
stop_pico();
|
|
pico_thread.WaitToEnd();
|
|
}
|
|
|
|
#else
|
|
|
|
#include "types.h"
|
|
|
|
bool start_pico() { return false; }
|
|
void stop_pico() { }
|
|
void net_thread_join() { }
|
|
void write_pico(u8 b) { }
|
|
int read_pico() { return -1; }
|
|
u32 net_eth_take(u8 *frame) { return 0; }
|
|
void pico_receive_eth_frame(const u8 *frame, u32 size) { }
|
|
|
|
#endif
|