/* Copyright 2023 flyinghead This file is part of Flycast. Flycast 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. Flycast 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 Flycast. If not, see . */ #include "netdimm.h" #include "naomi.h" #include "hw/holly/holly_intc.h" #include "hw/holly/sb.h" #include "hw/sh4/sh4_sched.h" #include "hw/mem/addrspace.h" #include "network/net_platform.h" #include "serialize.h" #include "naomi_roms.h" #include "naomi_regs.h" //#define HTTP_TRACE const char *SERVER_NAME = "vfnet.flyca.st"; NetDimm::NetDimm(u32 size) : GDCartridge(size) { if (serverIp == 0) { hostent *hp = gethostbyname(SERVER_NAME); if (hp != nullptr && hp->h_length > 0) { memcpy(&serverIp, hp->h_addr_list[0], sizeof(serverIp)); NOTICE_LOG(NAOMI, "%s IP is %x", SERVER_NAME, serverIp); } } } void NetDimm::Init(LoadProgress *progress, std::vector *digest) { GDCartridge::Init(progress, digest); dimmBufferOffset = dimm_data_size - 16_MB; finalTuned = strcmp(game->name, "vf4tuned") == 0; } bool NetDimm::Write(u32 offset, u32 size, u32 data) { // u8 b0 = data; // u8 b1 = data >> 8; // INFO_LOG(NAOMI, "Write<%d>%x: %x %c %c", size, offset, data, b0 == 0 ? ' ' : b0, b1 == 0 ? ' ' : b1); if (dimm_data != nullptr) { u32 addr = offset & (dimm_data_size - 1); memcpy(&dimm_data[addr], &data, std::min(size, dimm_data_size - addr)); } return true; } int NetDimm::schedCallback() { fd_set readFds {}; fd_set writeFds {}; fd_set exceptFds {}; int nfds = -1; for (Socket& socket : sockets) { if (socket.connecting || socket.sending) { FD_SET(socket.fd, &writeFds); FD_SET(socket.fd, &exceptFds); nfds = std::max(nfds, (int)socket.fd); } if (socket.receiving) { FD_SET(socket.fd, &readFds); FD_SET(socket.fd, &exceptFds); nfds = std::max(nfds, (int)socket.fd); } } if (nfds > -1) { if (SB_ISTEXT & 8) // holly_EXP_PCI return POLL_CYCLES; timeval tv {}; int rc = select(nfds + 1, &readFds, &writeFds, &exceptFds, &tv); for (Socket& socket : sockets) { if (socket.connecting) { if (rc > 0) { int so_error; socklen_t len = sizeof(so_error); getsockopt(socket.fd, SOL_SOCKET, SO_ERROR, (char *)&so_error, &len); if (so_error != L_EINPROGRESS && so_error != L_EWOULDBLOCK) { INFO_LOG(NAOMI, "connect(%d) completed -> %d", socket.fd, so_error); socket.connecting = false; socket.lastError = so_error; returnToNaomi(so_error != 0, &socket - &sockets[0] + 1, so_error); break; } } if (socket.connectTimeout > 0 && socket.connectTime + socket.connectTimeout <= sh4_sched_now64()) { WARN_LOG(NAOMI, "connect(%d) timeout", socket.fd); socket.connecting = false; socket.lastError = ECONNREFUSED; returnToNaomi(true, &socket - &sockets[0] + 1, ECONNREFUSED); // error code? break; } } else if (socket.receiving) { if (rc > 0) { rc = recv(socket.fd, (char *)socket.recvData, socket.recvLen, 0); if (rc == -1) { int error = get_last_error(); if (error != L_EAGAIN && error != L_EWOULDBLOCK) { socket.lastError = get_last_error(); socket.receiving = false; } } #ifdef HTTP_TRACE else if (rc > 0) { fwrite(socket.recvData, 1, rc, stdout); fflush(stdout); } #endif INFO_LOG(NAOMI, "recv(%d, %d) -> %d", (int)(&socket - &sockets[0] + 1), socket.recvLen, rc); if (rc >= 0) socket.receiving = false; if (!socket.receiving) { returnToNaomi(rc == -1, &socket - &sockets[0] + 1, rc); break; } } if (socket.recvTimeout > 0 && socket.recvTime + socket.recvTimeout <= sh4_sched_now64()) { WARN_LOG(NAOMI, "recv(%d) timeout", socket.fd); socket.receiving = false; socket.lastError = ETIMEDOUT; returnToNaomi(true, &socket - &sockets[0] + 1, ETIMEDOUT); // error code? break; } } else if (socket.sending) { if (rc > 0) { rc = send(socket.fd, (const char *)socket.sendData, socket.sendLen, 0); if (rc == -1) { int error = get_last_error(); if (error != L_EAGAIN && error != L_EWOULDBLOCK) { socket.lastError = get_last_error(); socket.sending = false; } } #ifdef HTTP_TRACE else if (rc > 0) { fwrite(socket.sendData, 1, rc, stdout); fflush(stdout); } #endif INFO_LOG(NAOMI, "send(%d, %d) -> %d", (int)(&socket - &sockets[0] + 1), socket.sendLen, rc); if (rc >= 0) socket.sending = false; if (!socket.sending) { returnToNaomi(rc == -1, &socket - &sockets[0] + 1, rc); break; } } if (socket.sendTimeout > 0 && socket.sendTime + socket.sendTimeout <= sh4_sched_now64()) { WARN_LOG(NAOMI, "send(%d) timeout", socket.fd); socket.sending = false; socket.lastError = ETIMEDOUT; returnToNaomi(true, &socket - &sockets[0] + 1, ETIMEDOUT); // error code? break; } } } return isBusy() ? POLL_CYCLES : SH4_MAIN_CLOCK; } if (SB_ISTEXT & 8) // holly_EXP_PCI return SH4_MAIN_CLOCK; // if (dnsInProgress) // { // NOTICE_LOG(NAOMI, "getIpByDns completed"); // returnToNaomi(false, 0, serverIp); // dnsInProgress = false; // return SH4_MAIN_CLOCK; // } // regularly peek the test request address peek(0xc01fc08); asic_RaiseInterrupt(holly_EXP_PCI); u32 testRequest = addrspace::read32(0xc01fc08); if (testRequest & 1) { // bios dimm test addrspace::write32(0xc01fc08, testRequest & ~1); bool isMem; char *p = (char *)addrspace::writeConst(0xc01fd00, isMem, 4); strcpy(p, "CHECKING DIMM BD"); p = (char *)addrspace::writeConst(0xc01fd10, isMem, 4); strcpy(p, "DIMM0 - GOOD"); p = (char *)addrspace::writeConst(0xc01fd20, isMem, 4); strcpy(p, "DIMM1 - GOOD"); p = (char *)addrspace::writeConst(0xc01fd30, isMem, 4); strcpy(p, "--- COMPLETED---"); addrspace::write32(0xc01fc0c, 0x0317a264); } else if (testRequest & 0x40) { // when entering vf4 test mode addrspace::write32(0xc01fc08, testRequest & ~0x40); addrspace::write32(0xc01fc60, htonl(0xc0a80101)); // FIXME ip address (192.168.1.1) addrspace::write32(0xc01fc0c, 0x03170264); INFO_LOG(NAOMI, "TEST REQUEST %x", testRequest); } else if (testRequest & 0x400) { // when entering vf4 test mode addrspace::write32(0xc01fc08, testRequest & ~0x400); addrspace::write32(0xc01fc70, 0x08080808); // FIXME dns2??? we might be off by one; and this would be dns1? addrspace::write32(0xc01fc0c, 0x03170264); INFO_LOG(NAOMI, "TEST REQUEST %x", testRequest); } else if (testRequest & 0x10000) { // bios network settings addrspace::write32(0xc01fc08, testRequest & ~0x10000); // TODO save to PIC? addrspace::write32(0xc01fc0c, 0x03170264); } else if (testRequest & 0x20000) { // network test addrspace::write32(0xc01fc08, testRequest & ~0x20000); bool isMem; char *p = (char *)addrspace::writeConst(0xc01fd00, isMem, 4); strcpy(p, "CHECKING NETWORK"); p = (char *)addrspace::writeConst(0xc01fd10, isMem, 4); strcpy(p, "PRETENDING... :P"); p = (char *)addrspace::writeConst(0xc01fd20, isMem, 4); strcpy(p, "--- COMPLETED---"); addrspace::write32(0xc01fc0c, 0x03170264); } else if (testRequest != 0) { addrspace::write32(0xc01fc08, 0); addrspace::write32(0xc01fc0c, 0x03170100); INFO_LOG(NAOMI, "TEST REQUEST %x", testRequest); } return SH4_MAIN_CLOCK; } void NetDimm::systemCmd(int cmd) { switch (cmd) { case 0xf: // startup NOTICE_LOG(NAOMI, "NetDIMM startup"); // bit 16,17: dimm0 size (none, 128, 256, 512) // bit 18,19: dimm1 size // bit 28: network enabled (network settings appear in bios menu) // bit 29: set // bit 30: gd-rom connected // bit 31: mobile/ppp network? // (| 30, 70, F0, 1F0, 3F0, 7F0) // | offset >> 20 (dimm buffers offset @ size - 16MB) // offset = (64MB << 0-5) - 16MB // vf4 forces this value to 0f000000 (256MB) if != 1f000000 (512MB) if (dimm_data_size == 512_MB) addrspace::write32(0xc01fc04, (3 << 16) | 0x70000000 | (dimmBufferOffset >> 20)); // dimm board config 1 x 512 MB else if (dimm_data_size == 256_MB) addrspace::write32(0xc01fc04, (2 << 16) | 0x70000000 | (dimmBufferOffset >> 20)); // dimm board config 1 x 256 MB else if (dimm_data_size == 128_MB) addrspace::write32(0xc01fc04, (1 << 16) | 0x70000000 | (dimmBufferOffset >> 20)); // dimm board config 1 x 128 MB else die("Unsupported dimm mem size"); addrspace::write32(0xc01fc0c, 0x3170000 | 0x264); // fw version | 100/264/364? addrspace::write32(0xc01fc10, 0); // additional pokes (initPoke_maybe) addrspace::write32(0xc01fc14, 1); // new in 3.17 addrspace::write32(0xc01fc20, 0x78000); addrspace::write32(0xc01fc24, 0x3e000a); addrspace::write32(0xc01fc28, 0x18077f); addrspace::write32(0xc01fc2c, 0x10014); // DIMM board serial Id { const u32 *serial = (u32 *)(getGameSerialId() + 0x20); // get only the serial id addrspace::write32(0xc01fc40, *serial++); addrspace::write32(0xc01fc44, *serial++); addrspace::write32(0xc01fc48, *serial++); addrspace::write32(0xc01fc4c, *serial++); } addrspace::write32(0xc01fc18, 0x10002); // net mode (2 or 4 is mobile), bit 16 dhcp? // network order addrspace::write32(0xc01fc60, htonl(0xc0a80101)); // ip address (192.168.1.1) addrspace::write32(0xc01fc64, htonl(0xffffff00)); // netmask addrspace::write32(0xc01fc68, htonl(0xc0a801fe)); // gateway 192.168.1.254 addrspace::write32(0xc01fc6c, htonl(0xc0a801fe)); // dns1 addrspace::write32(0xc01fc70, htonl(0x08080808)); // dns2 addrspace::write32(0xc01fc74, 0); // ? addrspace::write32(0xc01fc78, 0); // ? addrspace::write32(0xc01fc7c, 0); // ppp ip addr addrspace::write32(0xc01fc80, 0); // ppp param addrspace::write32(0xc01fc84, 0); // ? addrspace::write32(0xc01fc88, 0); // ? addrspace::write32(0xc01fc8c, 0); // ? addrspace::write32(0xc01fc90, 0); // ? addrspace::write32(0xc01fc94, 0); // ? // SET_BASE_ADDRESS(0c000000, 0) dimm_command = 0x8600; dimm_offsetl = 0; dimm_parameterl = 0; dimm_parameterh = 0x0c00; asic_RaiseInterrupt(holly_EXP_PCI); sh4_sched_request(schedId, SH4_MAIN_CLOCK); break; case 0: // nop case 1: // control read case 3: // set base address case 4: // peek8 case 5: // peek16 case 6: // peek32 case 8: // poke8 case 9: // poke16 case 10: // poke32 // These are callbacks from naomi INFO_LOG(NAOMI, "System callback command %x", cmd); break; default: WARN_LOG(NAOMI, "Unknown system command %x", cmd); break; } } void NetDimm::netCmd(int cmd) { u32 *buffer = (u32 *)&dimm_data[dimmBufferOffset + 0x800000 + 0x1000 * (dimm_command & 0xff)]; cmd = buffer[0]; switch (cmd) { case 0: // returnToNaomiRawCmd WARN_LOG(NAOMI, "netdimm: returnToNaomiRawCmd not implemented"); returnToNaomi(true, 0, -1); break; case 1: // accept WARN_LOG(NAOMI, "netdimm: accept not implemented"); returnToNaomi(true, buffer[1], -1); break; case 2: // bind WARN_LOG(NAOMI, "netdimm: bind not implemented"); returnToNaomi(true, buffer[1], -1); break; case 3: // close { const int sockidx = buffer[1]; const sock_t sockfd = getSocket(sockidx); int rc; if (sockfd == INVALID_SOCKET) { WARN_LOG(NAOMI, "closesocket(%d) invalid socket", sockidx); rc = -1; } else { rc = sockets[sockidx - 1].close(); INFO_LOG(NAOMI, "closesocket(%d) %d -> %d", sockidx, sockfd, rc); } returnToNaomi(rc != 0, sockidx, rc); break; } case 4: // connect { const int sockidx = buffer[1]; const sockaddr_in *addr = (const sockaddr_in *)&dimm_data[buffer[2]]; sock_t sockfd = getSocket(sockidx); int rc; if (sockfd == INVALID_SOCKET) { WARN_LOG(NAOMI, "connect(%d, %x) invalid socket", sockidx, htonl(addr->sin_addr.s_addr)); rc = -1; } else { //socklen_t len = (socklen_t)buffer[3]; sockaddr_in a {}; a.sin_family = AF_INET; // for some reason the family is in network order too. Just ignore it. a.sin_port = addr->sin_port; a.sin_addr.s_addr = addr->sin_addr.s_addr; rc = connect(sockfd, (sockaddr *)&a, sizeof(a)); if (rc == -1) { int error = get_last_error(); if (error == L_EINPROGRESS || error == L_EWOULDBLOCK) { sockets[sockidx - 1].connecting = true; sockets[sockidx - 1].connectTime = sh4_sched_now64(); sh4_sched_request(schedId, POLL_CYCLES); INFO_LOG(NAOMI, "connect(%d, %x:%d) delayed", sockidx, htonl(a.sin_addr.s_addr), htons(a.sin_port)); return; } else { sockets[sockidx - 1].lastError = error; } } else { if (finalTuned) set_non_blocking(sockfd); } INFO_LOG(NAOMI, "connect(%d, %x:%d) -> %d", sockidx, htonl(a.sin_addr.s_addr), htons(a.sin_port), rc); } returnToNaomi(rc != 0, sockidx, rc); break; } case 5: // getIpByDns { char *name = (char *)&dimm_data[buffer[1]]; INFO_LOG(NAOMI, "getIpByDns %s", name); //dnsInProgress = true; //sh4_sched_request(schedId, POLL_CYCLES * 10); //int len = buffer[2]; //u32 dns1 = buffer[3]; //u32 dns2 = buffer[4]; returnToNaomi(false, 0, serverIp); break; } case 6: // inet_addr WARN_LOG(NAOMI, "netdimm: inet_addr not implemented"); returnToNaomi(true, 0, -1); break; case 7: // ioctl WARN_LOG(NAOMI, "netdimm: ioctl not implemented"); returnToNaomi(true, buffer[1], -1); break; case 8: // listen WARN_LOG(NAOMI, "netdimm: listen not implemented"); returnToNaomi(true, buffer[1], -1); break; case 9: // recv { const int sockidx = buffer[1]; sock_t sockfd = getSocket(sockidx); int rc; if (sockfd == INVALID_SOCKET) { WARN_LOG(NAOMI, "recv(%d) invalid socket", sockidx); rc = -1; } else { u32 len = buffer[3]; u32 offset = buffer[2] & (dimm_data_size - 1); u8 *data = &dimm_data[offset]; rc = recv(sockfd, (char *)data, len, 0); if (rc == -1) { int error = get_last_error(); if (error == L_EAGAIN || error == L_EWOULDBLOCK) { sockets[sockidx - 1].receiving = true; sockets[sockidx - 1].recvTime = sh4_sched_now64(); sockets[sockidx - 1].recvData = data; sockets[sockidx - 1].recvLen = len; sh4_sched_request(schedId, POLL_CYCLES); INFO_LOG(NAOMI, "recv(%d, %d) delayed", sockidx, len); return; } else { sockets[sockidx - 1].lastError = get_last_error(); } } #ifdef HTTP_TRACE else if (rc > 0) { fwrite(data, 1, rc, stdout); fflush(stdout); } #endif INFO_LOG(NAOMI, "recv(%d, %d) -> %d", sockidx, len, rc); } returnToNaomi(rc == -1, sockidx, rc); break; } case 10: // send { const int sockidx = buffer[1]; sock_t sockfd = getSocket(sockidx); int rc; if (sockfd == INVALID_SOCKET) { INFO_LOG(NAOMI, "send(%d) invalid socket", sockidx); rc = -1; } else { u32 len = buffer[3]; u32 offset = buffer[2] & (dimm_data_size - 1); u8 *data = &dimm_data[offset]; rc = send(sockfd, (const char *)data, len, 0); if (rc == -1) { int error = get_last_error(); if (error == L_EAGAIN || error == L_EWOULDBLOCK) { sockets[sockidx - 1].sending = true; sockets[sockidx - 1].sendTime = sh4_sched_now64(); sockets[sockidx - 1].sendData = data; sockets[sockidx - 1].sendLen = len; sh4_sched_request(schedId, POLL_CYCLES); INFO_LOG(NAOMI, "send(%d, %d) delayed", sockidx, len); return; } else { sockets[sockidx - 1].lastError = get_last_error(); } } INFO_LOG(NAOMI, "send(%d, %d) -> %d", sockidx, len, rc); #ifdef HTTP_TRACE fwrite(data, 1, len, stdout); fflush(stdout); #endif } returnToNaomi(rc == -1, sockidx, rc); break; } case 11: // openSocket { const u32 domain = buffer[1]; const u32 type = buffer[2]; const u32 protocol = buffer[3]; const sock_t fd = socket(domain, type, protocol); int sockidx = -1; if (fd != INVALID_SOCKET) { // FIXME async mode still not right with FT if (!finalTuned) set_non_blocking(fd); size_t i = 0; for (; i < sockets.size(); i++) if (sockets[i].fd == INVALID_SOCKET) break; if (i == sockets.size()) sockets.emplace_back(fd); else sockets[i].fd = fd; sockidx = i + 1; } INFO_LOG(NAOMI, "openSocket(%d, %d, %d) %d -> %d", domain, type, protocol, fd, sockidx); returnToNaomi(sockidx == -1, 0, sockidx); break; } case 12: // netSelect { const u32 readFds = buffer[2]; const u32 writeFds = buffer[3]; const u32 exceptFds = buffer[4]; const u32 timeoutAddr = buffer[4]; int nfds = -1; fd_set read {}; fd_set write {}; fd_set except {}; timeval timeout; const auto& setFdsets = [this, buffer, &nfds](u32 fdsOffset, fd_set *fdset) { if (fdsOffset == 0) return; fd_set fds; memcpy(&fds, &dimm_data[fdsOffset & (dimm_data_size - 1)], std::min(sizeof(fds), 32)); const int count = buffer[1]; for (int i = 0; i < count; i++) { if (FD_ISSET(i, &fds)) { sock_t sockfd = getSocket(i); if (sockfd != INVALID_SOCKET) { FD_SET(sockfd, fdset); nfds = std::max(nfds, (int)sockfd); } } } }; setFdsets(readFds, &read); setFdsets(writeFds, &write); setFdsets(exceptFds, &except); if (timeoutAddr != 0) { timeout.tv_sec = *(u32 *)&dimm_data[timeoutAddr & (dimm_data_size - 1)]; timeout.tv_usec = *(u32 *)&dimm_data[(timeoutAddr + 4) & (dimm_data_size - 1)]; } int rc = select(nfds + 1, &read, &write, &except, timeoutAddr == 0 ? nullptr : &timeout); INFO_LOG(NAOMI, "select(%d, %x, %x, %x, %x) -> %d", nfds, readFds, writeFds, exceptFds, timeoutAddr, rc); returnToNaomi(rc == -1, 0, rc); break; } case 13: // shutdown (not implemented on real hardware) WARN_LOG(NAOMI, "netdimm: shutdown not implemented"); returnToNaomi(true, buffer[1], -3); break; case 14: // setsockopt WARN_LOG(NAOMI, "netdimm: setsockopt not implemented"); returnToNaomi(true, buffer[1], -1); break; case 15: // getsockopt WARN_LOG(NAOMI, "netdimm: getsockopt not implemented"); returnToNaomi(true, buffer[1], -1); break; case 16: // settimeout { const int sockidx = buffer[1]; sock_t sockfd = getSocket(sockidx); int rc; if (sockfd == INVALID_SOCKET) { WARN_LOG(NAOMI, "settimeout(%d) invalid socket", sockidx); rc = -1; } else { sockets[sockidx - 1].connectTimeout = (u64)buffer[2] * SH4_MAIN_CLOCK / 1000; sockets[sockidx - 1].sendTimeout = (u64)buffer[3] * SH4_MAIN_CLOCK / 1000; sockets[sockidx - 1].recvTimeout = (u64)buffer[4] * SH4_MAIN_CLOCK / 1000; INFO_LOG(NAOMI, "setTimeout(%d, %d, %d, %d)", sockidx, buffer[2], buffer[3], buffer[4]); rc = 0; } returnToNaomi(rc != 0, sockidx, 0); break; } case 17: // geterrno { int sockidx = buffer[1]; sock_t sockfd = getSocket(sockidx); if (sockfd != INVALID_SOCKET) { int rc = sockets[sockidx - 1].lastError; INFO_LOG(NAOMI, "geterrno(%d) -> %d", sockidx, rc); returnToNaomi(false, sockidx, rc); } else { returnToNaomi(true, sockidx, -1); } break; } case 18: // routeAdd WARN_LOG(NAOMI, "netdimm: routeAdd not implemented"); returnToNaomi(true, 0, -1); break; case 19: // routeDelete WARN_LOG(NAOMI, "netdimm: routeDelete not implemented"); returnToNaomi(true, 0, -1); break; case 20: // getParambyDHCP WARN_LOG(NAOMI, "netdimm: getParambyDHCP not implemented"); returnToNaomi(true, 0, -1); break; case 21: // modifyMyIPaddr WARN_LOG(NAOMI, "netdimm: modifyMyIPaddr not implemented"); returnToNaomi(true, 0, -1); break; case 22: // recvfrom WARN_LOG(NAOMI, "netdimm: recvfrom not implemented"); returnToNaomi(true, buffer[1], -1); break; case 23: // sendto WARN_LOG(NAOMI, "netdimm: sendto not implemented"); returnToNaomi(true, buffer[1], -1); break; default: WARN_LOG(NAOMI, "netdimm: Invalid Net command: %d", cmd); returnToNaomi(true, 0, 0); break; } } void NetDimm::process() { INFO_LOG(NAOMI, "NetDIMM cmd %04x sock %d offset %04x paramh/l %04x %04x", (dimm_command >> 9) & 0x3f, dimm_command & 0xff, dimm_offsetl, dimm_parameterh, dimm_parameterl); int cmdGroup = (dimm_command >> 13) & 3; int cmd = (dimm_command >> 9) & 0xf; switch (cmdGroup) { case 0: // system commands systemCmd(cmd); break; case 1: // network client netCmd(cmd); break; default: WARN_LOG(NAOMI, "Unknown DIMM command group %d cmd %x", cmdGroup, cmd); returnToNaomi(true, 0, -1); break; } } void NetDimm::Deserialize(Deserializer &deser) { GDCartridge::Deserialize(deser); for (Socket& socket : sockets) socket.close(); if (deser.version() >= Deserializer::V36 && deser.version() < Deserializer::V53) { // moved to parent class in v53 deser >> dimm_command; deser >> dimm_offsetl; deser >> dimm_parameterl; deser >> dimm_parameterh; sh4_sched_deserialize(deser, schedId); } }