diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt index 149d601135..0877137a46 100644 --- a/Core/CMakeLists.txt +++ b/Core/CMakeLists.txt @@ -504,6 +504,7 @@ add_library(Core STATIC HLE/AdhocTransport.h HLE/AdhocTransportP2P.cpp HLE/AdhocTransportRelay.cpp + HLE/AdhocTransportNetSim.cpp HLE/proAdhoc.h HLE/proAdhoc.cpp HLE/proAdhocServer.h diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index dfec2b04c7..534312fe12 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -262,6 +262,9 @@ static const CommandLineParam g_autoParams[] = { {POFF(speedPercent), CmdParamType::Int, "speed", '\0', "With --realtime, run at this percentage of PSP speed (alternate speed 1)", CmdLineMode::Headless}, {POFF(adhocServer), CmdParamType::Bool, "adhoc-server", '\0', "Run the built-in ad hoc server"}, {POFF(nickname), CmdParamType::String, "nickname", '\0', "The PSP's nickname (System settings), for this run only"}, + {POFF(adhocDelayMs), CmdParamType::Int, "adhoc-delay", '\0', "Delay ad hoc datagrams received by MS milliseconds of emulated time, to simulate a distant player"}, + {POFF(adhocJitterMs), CmdParamType::Int, "adhoc-jitter", '\0', "With --adhoc-delay, up to MS more at random"}, + {POFF(adhocLossPercent), CmdParamType::Int, "adhoc-loss", '\0', "Drop this percentage of the ad hoc datagrams received"}, {POFF(lagSync), CmdParamType::Bool, "lag-sync", '\0', "Keep emulated time in step with the wall clock every millisecond, not every frame (the Force real clock sync setting)"}, {POFF(frameStats), CmdParamType::Bool, "frame-stats", '\0', "Print flips per emulated second at the end of the run, counted from --state's load", CmdLineMode::Headless}, {POFF(maxScreenshotError), CmdParamType::Double, "max-mse", '\0', "Maximum allowed MSE error for screenshot comparison", CmdLineMode::Headless}, @@ -583,6 +586,18 @@ void CommandLineOptions::ApplyToConfig() const { g_Config.sNickName = nickname.value(); g_Config.DoNotSaveSetting(&g_Config.sNickName); } + if (adhocDelayMs.has_value()) { + g_Config.iAdhocSimDelayMs = adhocDelayMs.value(); + g_Config.DoNotSaveSetting(&g_Config.iAdhocSimDelayMs); + } + if (adhocJitterMs.has_value()) { + g_Config.iAdhocSimJitterMs = adhocJitterMs.value(); + g_Config.DoNotSaveSetting(&g_Config.iAdhocSimJitterMs); + } + if (adhocLossPercent.has_value()) { + g_Config.iAdhocSimLossPercent = adhocLossPercent.value(); + g_Config.DoNotSaveSetting(&g_Config.iAdhocSimLossPercent); + } if (lagSync.has_value()) { g_Config.bForceLagSync = lagSync.value(); g_Config.DoNotSaveSetting(&g_Config.bForceLagSync); diff --git a/Core/CmdLine.h b/Core/CmdLine.h index c8352e3efe..fb5702d60e 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -211,6 +211,9 @@ struct CommandLineOptions { std::optional adhocServer; std::optional lagSync; std::optional nickname; + std::optional adhocDelayMs; + std::optional adhocJitterMs; + std::optional adhocLossPercent; std::optional screenshotFilename; std::optional screenshotFilenameSave; diff --git a/Core/Config.cpp b/Core/Config.cpp index a5b8ace797..17fad32ebc 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -1419,6 +1419,9 @@ static constexpr ConfigSetting networkSettings[] = { ConfigSetting("PortOffset", SETTING(g_Config, iPortOffset), 10000, CfgFlag::PER_GAME) .Label(I18NCat::NETWORKING, "Port offset").Range(0, 60000, 100), ConfigSetting("PrimaryDNSServer", SETTING(g_Config, sInfrastructureDNSServer), "67.222.156.250", CfgFlag::PER_GAME), + ConfigSetting("AdhocSimDelayMs", SETTING(g_Config, iAdhocSimDelayMs), 0, CfgFlag::DEFAULT), + ConfigSetting("AdhocSimJitterMs", SETTING(g_Config, iAdhocSimJitterMs), 0, CfgFlag::DEFAULT), + ConfigSetting("AdhocSimLossPercent", SETTING(g_Config, iAdhocSimLossPercent), 0, CfgFlag::DEFAULT), ConfigSetting("MinTimeout", SETTING(g_Config, iMinTimeout), 0, CfgFlag::PER_GAME) .Label(I18NCat::NETWORKING, "Minimum Timeout", "Minimum Timeout (override in ms, 0 = default)").Range(0, 15000, 50).Units("ms"), ConfigSetting("ForcedFirstConnect", SETTING(g_Config, bForcedFirstConnect), false, CfgFlag::PER_GAME) diff --git a/Core/Config.h b/Core/Config.h index 58f9d51977..afa427572e 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -630,6 +630,10 @@ public: bool bInfrastructureAutoDNS; bool bAllowSavestateWhileConnected; // Developer option, ini-only. No normal users need this, it's always wrong to save/load state when online. bool bAllowSpeedControlWhileConnected; // Useful in some games but not recommended. + // Developer options, ini-only: simulate a slower network for the ad hoc datagrams received. + int iAdhocSimDelayMs; // Added to every datagram's delivery, in emulated time. + int iAdhocSimJitterMs; // Up to this much more, at random (order is kept). + int iAdhocSimLossPercent; // Datagrams dropped. std::string sAdhocServerListUrl; std::vector vCustomAdhocServerList; diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index 110af2d0a8..6ac5e84aac 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -652,6 +652,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 7d6b3c8cc1..2ce11d6a71 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -533,6 +533,9 @@ HLE\Libraries + + HLE\Libraries + HLE\Libraries diff --git a/Core/HLE/AdhocTransport.h b/Core/HLE/AdhocTransport.h index fff2873303..9655184ecc 100644 --- a/Core/HLE/AdhocTransport.h +++ b/Core/HLE/AdhocTransport.h @@ -19,6 +19,10 @@ #include "Core/HLE/proAdhoc.h" #include +#include +#include +#include +#include #include #include "Core/HLE/sceNetAdhoc.h" @@ -32,8 +36,8 @@ struct AdhocPollState { // How ad hoc sockets reach the other consoles: through the aemu_postoffice relay, or directly over the // host's own UDP/TCP sockets (P2P). The sceNetAdhoc functions do everything a PSP does the same either -// way, and call one of these for the parts that depend on the transport. Transport() picks the one in -// use, which follows serverHasRelay. +// way, and call one of these for the parts that depend on the transport. Transport() is the one in use, +// made when ad hoc starts (following serverHasRelay) and freed when it stops. // // Ids are PSP socket IDs (1-based slots in adhocSockets). Unless said otherwise, the arguments, the // socket and its alerts have been checked. @@ -46,9 +50,6 @@ public: // Called every millisecond of emulated time while the ad hoc server connection is up, for work that // happens on a PSP whatever the game calls. virtual void Poll() {} - // Whether the connection to a local ad hoc server should be bound to our own local IP, so that - // instances on one machine look like different consoles to it. - virtual bool BindServerConnectionToLocalIp() = 0; // PDP @@ -76,6 +77,9 @@ public: virtual int PtpClose(int id) = 0; // The rest of sceNetAdhocPtpSend / PtpRecv / PtpAccept, and their blocking retries. virtual int PtpSend(int id, const char *data, int *len, int timeout, int flag) = 0; + // Sends what it can of data at once, from outside a syscall (the network simulation's delayed sends): + // the bytes sent, 0 if none can go now, < 0 if the connection is gone. + virtual int PtpSendNow(int id, const char *data, int len) = 0; virtual int PtpRecv(int id, void *buf, int *len, int timeout, int flag) = 0; virtual int PtpAccept(int id, SceNetEtherAddr *addr, uint16_t *port, int timeout, int flag) = 0; virtual int DoBlockingPtpSend(AdhocSocketRequest &req, s64 &result) = 0; @@ -116,6 +120,7 @@ public: #define RELAY_FAILURE_DISABLE_THRESHOLD_SEC 15 extern std::chrono::time_point relayLastFailure; +extern bool serverHasRelay; extern bool trackingRelayFailure; extern bool relayDisabled; extern bool relayFirstConnect; @@ -153,6 +158,7 @@ public: int PtpListen(const SceNetEtherAddr *saddr, int sport, int bufsize, int rexmt_int, int rexmt_cnt, int backlog, bool isClient, int flag) override; int PtpClose(int id) override; int PtpSend(int id, const char *data, int *len, int timeout, int flag) override; + int PtpSendNow(int id, const char *data, int len) override; int PtpRecv(int id, void *buf, int *len, int timeout, int flag) override; int PtpAccept(int id, SceNetEtherAddr *addr, uint16_t *port, int timeout, int flag) override; int DoBlockingPtpSend(AdhocSocketRequest &req, s64 &result) override; @@ -166,7 +172,6 @@ public: int PollWait(SceNetAdhocPollSd *sds, int count, int timeout, AdhocPollState *states) override; - bool BindServerConnectionToLocalIp() override { return false; } bool PdpReadyToSend(AdhocSocket *sock) override { return true; } // A connection's handle only comes once it's up, so there's no in-between to check. int PtpConnectVerify(int id, sockaddr_in *sin, int ret, int *error) override { return ret; } @@ -189,6 +194,7 @@ public: int PtpListen(const SceNetEtherAddr *saddr, int sport, int bufsize, int rexmt_int, int rexmt_cnt, int backlog, bool isClient, int flag) override; int PtpClose(int id) override; int PtpSend(int id, const char *data, int *len, int timeout, int flag) override; + int PtpSendNow(int id, const char *data, int len) override; int PtpRecv(int id, void *buf, int *len, int timeout, int flag) override; int PtpAccept(int id, SceNetEtherAddr *addr, uint16_t *port, int timeout, int flag) override; int DoBlockingPtpSend(AdhocSocketRequest &req, s64 &result) override; @@ -202,7 +208,6 @@ public: int PollWait(SceNetAdhocPollSd *sds, int count, int timeout, AdhocPollState *states) override; - bool BindServerConnectionToLocalIp() override { return true; } bool PdpReadyToSend(AdhocSocket *sock) override; int PtpConnectVerify(int id, sockaddr_in *sin, int ret, int *error) override; bool PtpConnectPending(int error) override; @@ -211,4 +216,69 @@ public: bool PtpPeerClosed(AdhocSocket *sock) override; }; +// In AdhocTransportNetSim.cpp. The developer options for a slow network (--adhoc-delay, --adhoc-jitter, +// --adhoc-loss) as a transport around the real one, which it owns. Everything it doesn't delay goes +// straight to the transport inside. +class NetSimTransport : public AdhocTransport { +public: + // seed is for the random numbers, which differ per instance. + NetSimTransport(std::unique_ptr inner, uint32_t seed); + // Whether any of the options are on. They're read when ad hoc starts. + static bool Enabled(); + + void CloseHostSocket(AdhocSocket *sock) override; + void Poll() override; + + int PdpCreate(const SceNetEtherAddr *saddr, int port, int bufferSize, u32 flag, bool isClient, bool enetReliable) override { return inner_->PdpCreate(saddr, port, bufferSize, flag, isClient, enetReliable); } + int PdpDelete(int id) override; + int PdpSendTo(int id, const SceNetEtherAddr *mac, uint16_t dport, const sockaddr_in &target, const void *data, int len, int *error) override { return inner_->PdpSendTo(id, mac, dport, target, data, len, error); } + bool PdpTakeDatagram(int id, SceNetEtherAddr *mac, uint16_t *port, std::vector &data) override; + bool PdpReadyToSend(AdhocSocket *sock) override { return inner_->PdpReadyToSend(sock); } + + int PtpOpen(const SceNetEtherAddr *saddr, int sport, const SceNetEtherAddr *daddr, int dport, int bufsize, int rexmt_int, int rexmt_cnt, int flag) override { return inner_->PtpOpen(saddr, sport, daddr, dport, bufsize, rexmt_int, rexmt_cnt, flag); } + int PtpListen(const SceNetEtherAddr *saddr, int sport, int bufsize, int rexmt_int, int rexmt_cnt, int backlog, bool isClient, int flag) override { return inner_->PtpListen(saddr, sport, bufsize, rexmt_int, rexmt_cnt, backlog, isClient, flag); } + int PtpClose(int id) override; + int PtpSend(int id, const char *data, int *len, int timeout, int flag) override; + int PtpSendNow(int id, const char *data, int len) override { return inner_->PtpSendNow(id, data, len); } + int PtpRecv(int id, void *buf, int *len, int timeout, int flag) override { return inner_->PtpRecv(id, buf, len, timeout, flag); } + int PtpAccept(int id, SceNetEtherAddr *addr, uint16_t *port, int timeout, int flag) override { return inner_->PtpAccept(id, addr, port, timeout, flag); } + int DoBlockingPtpSend(AdhocSocketRequest &req, s64 &result) override { return inner_->DoBlockingPtpSend(req, result); } + int DoBlockingPtpRecv(AdhocSocketRequest &req, s64 &result) override { return inner_->DoBlockingPtpRecv(req, result); } + int DoBlockingPtpAccept(AdhocSocketRequest &req, s64 &result) override { return inner_->DoBlockingPtpAccept(req, result); } + int PtpConnect(int id, const sockaddr_in &sin, int *error) override { return inner_->PtpConnect(id, sin, error); } + int PtpConnectPoll(AdhocSocket *sock, int *error) override { return inner_->PtpConnectPoll(sock, error); } + int PtpConnectVerify(int id, sockaddr_in *sin, int ret, int *error) override { return inner_->PtpConnectVerify(id, sin, ret, error); } + bool PtpConnectPending(int error) override { return inner_->PtpConnectPending(error); } + bool PtpConnectionUp(AdhocSocket *sock) override { return inner_->PtpConnectionUp(sock); } + bool PtpConnectedSinceOpen(AdhocSocket *sock) override { return inner_->PtpConnectedSinceOpen(sock); } + void PtpUpdateRecvStat(AdhocSocket *sock) override; + int PtpFlush(int socketId) override; + + int PollWait(SceNetAdhocPollSd *sds, int count, int timeout, AdhocPollState *states) override { return inner_->PollWait(sds, count, timeout, states); } + void PollCheckConnecting(int id, AdhocSocket *sock, const AdhocPollState &state) override { inner_->PollCheckConnecting(id, sock, state); } + bool PtpPeerClosed(AdhocSocket *sock) override { return inner_->PtpPeerClosed(sock); } + +private: + struct HeldDatagram { + u64 arrivalUs; + SceNetEtherAddr mac; + uint16_t port; + std::vector data; + }; + struct HeldSend { + u64 departureUs; + std::vector data; + }; + + u64 Delay(); + bool Lost(); + u32 HeldBytes(const AdhocSocket *sock) const; + void SendHeld(int id, bool all); + + std::unique_ptr inner_; + std::map> datagrams_; + std::map> sends_; + std::mt19937 random_; +}; + AdhocTransport &Transport(); diff --git a/Core/HLE/AdhocTransportNetSim.cpp b/Core/HLE/AdhocTransportNetSim.cpp new file mode 100644 index 0000000000..7866c5eddc --- /dev/null +++ b/Core/HLE/AdhocTransportNetSim.cpp @@ -0,0 +1,173 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program 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, version 2.0 or later versions. + +// This program 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 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +// The network simulation transport (see NetSimTransport in AdhocTransport.h). Times are emulated, so a +// delay is the same at any speed. +// - PDP: a datagram the transport inside delivers is held until its arrival, then handed over, and goes +// into the socket's queue, where it may not fit, as if it had just arrived. +// - PTP: a send is accepted at once and held until its departure, then sent from Poll(). With the same +// options on every instance that gives the same round trip as delaying the receive. Nothing is lost +// on a connection: a "lost" send goes out one retransmit interval (from PtpOpen) later instead. +// Jitter delays without reordering. The random numbers are seeded per instance when ad hoc starts. + +#include +#include + +#include "Core/Config.h" +#include "Core/CoreTiming.h" +#include "Core/HLE/HLE.h" +#include "Core/HLE/AdhocTransport.h" + +NetSimTransport::NetSimTransport(std::unique_ptr inner, uint32_t seed) : inner_(std::move(inner)), random_(seed) {} + +bool NetSimTransport::Enabled() { + return g_Config.iAdhocSimDelayMs > 0 || g_Config.iAdhocSimJitterMs > 0 || g_Config.iAdhocSimLossPercent > 0; +} + +u64 NetSimTransport::Delay() { + u64 delayUs = (u64)std::max(g_Config.iAdhocSimDelayMs, 0) * 1000; + if (g_Config.iAdhocSimJitterMs > 0) { + delayUs += random_() % ((u64)g_Config.iAdhocSimJitterMs * 1000 + 1); + } + return delayUs; +} + +bool NetSimTransport::Lost() { + return (int)(random_() % 100) < g_Config.iAdhocSimLossPercent; +} + +u32 NetSimTransport::HeldBytes(const AdhocSocket *sock) const { + u32 bytes = 0; + auto it = sends_.find(sock); + if (it != sends_.end()) { + for (const HeldSend &send : it->second) { + bytes += (u32)send.data.size(); + } + } + return bytes; +} + +void NetSimTransport::CloseHostSocket(AdhocSocket *sock) { + datagrams_.erase(sock); + sends_.erase(sock); + inner_->CloseHostSocket(sock); +} + +void NetSimTransport::Poll() { + inner_->Poll(); + for (int i = 0; i < MAX_SOCKET; i++) { + if (adhocSockets[i] != NULL && adhocSockets[i]->type == SOCK_PTP) { + SendHeld(i + 1, false); + } + } +} + +int NetSimTransport::PdpDelete(int id) { + datagrams_.erase(adhocSockets[id - 1]); + return inner_->PdpDelete(id); +} + +bool NetSimTransport::PdpTakeDatagram(int id, SceNetEtherAddr *mac, uint16_t *port, std::vector &data) { + std::deque &held = datagrams_[adhocSockets[id - 1]]; + const u64 now = CoreTiming::GetGlobalTimeUs(); + HeldDatagram datagram{}; + while (inner_->PdpTakeDatagram(id, &datagram.mac, &datagram.port, datagram.data)) { + if (Lost()) { + continue; + } + datagram.arrivalUs = now + Delay(); + if (!held.empty()) { + datagram.arrivalUs = std::max(datagram.arrivalUs, held.back().arrivalUs); + } + held.push_back(datagram); + } + if (held.empty() || held.front().arrivalUs > now) { + return false; + } + *mac = held.front().mac; + *port = held.front().port; + data = std::move(held.front().data); + held.pop_front(); + return true; +} + +int NetSimTransport::PtpClose(int id) { + // What's held goes out first, as far as it can. + SendHeld(id, true); + sends_.erase(adhocSockets[id - 1]); + return inner_->PtpClose(id); +} + +int NetSimTransport::PtpSend(int id, const char *data, int *len, int timeout, int flag) { + AdhocSocket *sock = adhocSockets[id - 1]; + std::deque &held = sends_[sock]; + u64 delayUs = Delay(); + if (Lost()) { + delayUs += sock->retry_interval; + } + u64 departureUs = CoreTiming::GetGlobalTimeUs() + delayUs; + if (!held.empty()) { + departureUs = std::max(departureUs, held.back().departureUs); + } + held.push_back({ departureUs, std::vector(data, data + *len) }); + hleEatMicro(122); + return 0; +} + +// Sends what's due, or with all, everything that can go now. +void NetSimTransport::SendHeld(int id, bool all) { + auto it = sends_.find(adhocSockets[id - 1]); + if (it == sends_.end()) { + return; + } + const u64 now = CoreTiming::GetGlobalTimeUs(); + std::deque &held = it->second; + while (!held.empty() && (all || held.front().departureUs <= now)) { + std::vector &data = held.front().data; + int sent = inner_->PtpSendNow(id, (const char *)data.data(), (int)data.size()); + if (sent < 0) { + // The connection is gone: DISCONNECTED from the game's next call, as when PtpSend fails. + adhocSockets[id - 1]->data.ptp.state = ADHOC_PTP_STATE_CLOSED; + held.clear(); + return; + } + if (sent == 0) { + return; + } + data.erase(data.begin(), data.begin() + sent); + if (data.empty()) { + held.pop_front(); + } + } +} + +void NetSimTransport::PtpUpdateRecvStat(AdhocSocket *sock) { + inner_->PtpUpdateRecvStat(sock); + // Held sends haven't gone yet. + sock->data.ptp.snd_sb_cc = HeldBytes(sock); +} + +int NetSimTransport::PtpFlush(int socketId) { + // socketId is the transport's own ID for the socket. + for (int i = 0; i < MAX_SOCKET; i++) { + AdhocSocket *sock = adhocSockets[i]; + if (sock != NULL && sock->type == SOCK_PTP && sock->data.ptp.id == socketId && HeldBytes(sock) != 0) { + return EAGAIN; + } + } + return inner_->PtpFlush(socketId); +} diff --git a/Core/HLE/AdhocTransportP2P.cpp b/Core/HLE/AdhocTransportP2P.cpp index f449c0dabc..815c991453 100644 --- a/Core/HLE/AdhocTransportP2P.cpp +++ b/Core/HLE/AdhocTransportP2P.cpp @@ -929,6 +929,24 @@ int P2PTransport::PtpListen(const SceNetEtherAddr *saddr, int sport, int bufsize return hleLogDebug(Log::sceNet, SCE_NET_ADHOC_ERROR_SOCKET_ID_NOT_AVAIL, "socket id not available"); } +int P2PTransport::PtpSendNow(int id, const char *data, int len) { + AdhocSocket *socket = adhocSockets[id - 1]; + int error = 0; + auto &ptpsocket = socket->data.ptp; + int sent = ptpSendToPeer(socket, data, len, &error); + if (sent > 0) { + // As in PtpSend. + if (ptpsocket.state == ADHOC_PTP_STATE_SYN_SENT) + ptpSetEstablished(socket); + return sent; + } + // Full, or still connecting: try again later, as PtpSend does. + if (sent == SOCKET_ERROR && (error == EAGAIN || error == EWOULDBLOCK || (ptpsocket.state == ADHOC_PTP_STATE_SYN_SENT && (error == ENOTCONN || connectInProgress(error))))) { + return 0; + } + return -1; +} + int P2PTransport::PtpSend(int id, const char *data, int *len, int timeout, int flag) { auto socket = adhocSockets[id - 1]; auto& ptpsocket = socket->data.ptp; diff --git a/Core/HLE/AdhocTransportRelay.cpp b/Core/HLE/AdhocTransportRelay.cpp index 4c7df32960..e31e4b0200 100644 --- a/Core/HLE/AdhocTransportRelay.cpp +++ b/Core/HLE/AdhocTransportRelay.cpp @@ -269,6 +269,14 @@ static int ptp_send_postoffice(int idx, const void *data, int *len) { return 0; } +int RelayTransport::PtpSendNow(int id, const char *data, int len) { + int result = ptp_send_postoffice(id - 1, data, &len); + if (result == 0) { + return len; + } + return result == SCE_NET_ADHOC_ERROR_WOULD_BLOCK ? 0 : -1; +} + int RelayTransport::DoBlockingPtpSend(AdhocSocketRequest &req, s64 &result) { auto sock = adhocSockets[req.id - 1]; auto& ptpsocket = sock->data.ptp; diff --git a/Core/HLE/proAdhoc.cpp b/Core/HLE/proAdhoc.cpp index 6a91d3f4fb..6b301d54c8 100644 --- a/Core/HLE/proAdhoc.cpp +++ b/Core/HLE/proAdhoc.cpp @@ -1490,7 +1490,7 @@ static void StartAdhocctlLogin() { // If Server is at localhost and we are in P2P mode, try to bind socket to specific adapter before connecting to prevent 2nd instance being recognized as already existing 127.0.0.1 by adhocctl server // (may not works in WinXP/2003 for IPv4 due to "Weak End System" model) struct adhocctl_addr_v4 bindAddr = {0}; - const bool bind = isLocalServer && Transport().BindServerConnectionToLocalIp(); + const bool bind = isLocalServer && !serverHasRelay; if (bind) { bindAddr.ip = g_localhostIP.in.sin_addr.s_addr; bindAddr.port = 0; diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 84cc2dbd47..4885332a7a 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -45,6 +45,7 @@ #include "Core/Config.h" #include "Core/CoreTiming.h" #include "Core/Core.h" +#include "Core/Instance.h" #include "Core/Reporting.h" #include "Core/MemMapHelpers.h" @@ -379,14 +380,12 @@ int NetAdhocGameMode_DeleteMaster(); int NetAdhocctl_ExitGameMode(); -static RelayTransport relayTransport; -static P2PTransport p2pTransport; +// Only used on the CPU thread, between SetupAdhocRelayMode() and freeing it after the sockets. +static std::unique_ptr transport; AdhocTransport &Transport() { - if (serverHasRelay) { - return relayTransport; - } - return p2pTransport; + _dbg_assert_(transport); + return *transport; } // Forward declarations for the savestate mechanism (the matching is sadly not inside its own section) @@ -601,7 +600,10 @@ static const int ADHOCCTL_POLL_INTERVAL_US = 1000; static void __AdhocctlPoll(u64 userdata, int cyclesLate) { friendFinderPoll(); - Transport().Poll(); + // Adhocctl stays connected through sceNetAdhocTerm, which frees the transport. + if (transport) { + transport->Poll(); + } pdpFillQueues(); if (friendFinderRunning) { CoreTiming::ScheduleEvent(usToCycles(ADHOCCTL_POLL_INTERVAL_US) - cyclesLate, adhocctlPollEvent, 0); @@ -1438,6 +1440,16 @@ static void SetupAdhocRelayMode() { break; } + if (serverHasRelay) { + transport = std::make_unique(); + } else { + transport = std::make_unique(); + } + if (NetSimTransport::Enabled()) { + // The random numbers are the same for a run each time, but not on every instance. + transport = std::make_unique(std::move(transport), PPSSPP_ID); + } + netAdhocUseUdp = g_Config.bAdhocUseUdp; netAdhocRelayModeEnetPdpReliable = g_Config.bAdhocRelayModeEnetPdpReliable; @@ -1643,7 +1655,8 @@ void __NetAdhocDoState(PointerWrap &p) { StartFriendFinder(); } if (!netAdhocInited && cur_netAdhocInited) { - // cleanup adhoc + // cleanup adhoc (the sockets are gone already) + transport.reset(); if (aemuPostofficeEnetThread.joinable()) { aemuPostofficeEnetPumping = false; aemuPostofficeEnetThread.join(); @@ -3465,6 +3478,7 @@ int NetAdhoc_Term() { // Delete Adhoc Sockets deleteAllAdhocSockets(); nextAdhocSocketSlot = 0; + transport.reset(); // Terminate Internet Library //sceNetInetTerm(); diff --git a/Tools/adhoc_session.py b/Tools/adhoc_session.py index 84cbe12968..9779c1fc13 100644 --- a/Tools/adhoc_session.py +++ b/Tools/adhoc_session.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 # Runs several PPSSPP headless instances of a game against each other over ad hoc, and drives them over # the WebSocket debugger: press buttons, take screenshots, read frame rates. For testing a game's -# multiplayer. See docs/adhoc-multiplayer-testing.md. +# multiplayer, and for measuring what network delay it tolerates. See docs/adhoc-multiplayer-testing.md. # -# python3 Tools/adhoc_session.py start ~/PSP/GAME.iso -n 2 [--state FILE] +# python3 Tools/adhoc_session.py start ~/PSP/GAME.iso -n 2 [--delay 50] [--state FILE] # python3 Tools/adhoc_session.py press cross -i 2 # python3 Tools/adhoc_session.py wait 3 # python3 Tools/adhoc_session.py shot lobby @@ -261,6 +261,12 @@ def cmd_start(args): if n == 1: cmd.append("--adhoc-server") cmd.append("--nickname=" + (names[n - 1] if n <= len(names) else "PPSSPP %d" % n)) + if args.delay: + cmd.append("--adhoc-delay=%d" % args.delay) + if args.jitter: + cmd.append("--adhoc-jitter=%d" % args.jitter) + if args.loss: + cmd.append("--adhoc-loss=%d" % args.loss) if args.log: cmd.append("--log") if args.state: @@ -283,10 +289,12 @@ def cmd_start(args): time.sleep(0.1) if inst["port"] is None: sys.exit("Instance %d didn't open a debugger port; see %s" % (inst["n"], inst["log"])) - session = {"game": args.game, "instances": instances, "lagSync": not args.no_lag_sync, "p2p": args.p2p, "speed": args.speed} + session = {"game": args.game, "instances": instances, "delay": args.delay, "jitter": args.jitter, + "loss": args.loss, "lagSync": not args.no_lag_sync, "p2p": args.p2p, "speed": args.speed} with open(session_file(args), "w") as f: json.dump(session, f, indent=1) - print("Started %d instances (%s), session in %s" % (args.n, "P2P" if args.p2p else "relay", args.dir)) + print("Started %d instances (%s%s), session in %s" % (args.n, "P2P" if args.p2p else "relay", + ", delay %d ms" % args.delay if args.delay else "", args.dir)) def cmd_stop(args): @@ -556,6 +564,9 @@ def build_parser(): p.add_argument("-n", type=int, default=2, help="Number of instances (default 2)") p.add_argument("--p2p", action="store_true", help="Talk directly instead of through the relay (needs a loopback address per instance)") p.add_argument("--names", help="Comma-separated nicknames for the instances (default: PPSSPP 1, PPSSPP 2, ...)") + p.add_argument("--delay", type=int, default=0, help="Simulated delay in emulated ms on received ad hoc datagrams (each way)") + p.add_argument("--jitter", type=int, default=0, help="Up to this many ms more, at random") + p.add_argument("--loss", type=int, default=0, help="Percentage of received ad hoc datagrams dropped") p.add_argument("--no-lag-sync", action="store_true", help="Pace once per frame, like the app's default") p.add_argument("--no-sync", action="store_true", help="Let each instance run at its own pace instead of keeping them in step in emulated time") p.add_argument("--speed", type=int, default=100, help="Run at this percentage of PSP speed (scripts count emulated time)") diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj index 1306e0b5cf..51b0d350ac 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj +++ b/UWP/CoreUWP/CoreUWP.vcxproj @@ -539,6 +539,7 @@ + diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters index d47fbb86c8..4b483af759 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj.filters +++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters @@ -142,6 +142,7 @@ + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index b356ab1045..56b8712f5b 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -810,6 +810,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Core/HLE/NetAdhocCommon.cpp \ $(SRC)/Core/HLE/AdhocTransportP2P.cpp \ $(SRC)/Core/HLE/AdhocTransportRelay.cpp \ + $(SRC)/Core/HLE/AdhocTransportNetSim.cpp \ $(SRC)/Core/HLE/sceNetAdhoc.cpp \ $(SRC)/ext/aemu_postoffice/client/postoffice.c \ $(SRC)/ext/aemu_postoffice/client/postoffice_mem_stdc.c \ diff --git a/docs/adhoc-multiplayer-testing.md b/docs/adhoc-multiplayer-testing.md index e60d580230..7bd188d2b3 100644 --- a/docs/adhoc-multiplayer-testing.md +++ b/docs/adhoc-multiplayer-testing.md @@ -115,6 +115,35 @@ sudo ifconfig lo0 alias 127.0.0.3 up `adhoc_session.py start --p2p` checks for them and prints these commands if any are missing. +## Simulating a slow network + +Three options delay or drop the ad hoc datagrams an instance receives, to see how a game copes with a +distant player or a bad connection. They work the same in relay and P2P mode, and are also in the ini, +under `[Network]`, for the app. + +| Option | ini | What it does | +|---|---|---| +| `--adhoc-delay=MS` | `AdhocSimDelayMs` | Each datagram reaches the game MS milliseconds after it arrived. | +| `--adhoc-jitter=MS` | `AdhocSimJitterMs` | Up to MS more, at random. Datagrams stay in order. | +| `--adhoc-loss=PERCENT` | `AdhocSimLossPercent` | Datagrams dropped. | + +`adhoc_session.py start` takes them as `--delay`, `--jitter` and `--loss`, and passes them to every +instance; `Tools/adhoc_multi.py` passes them through to the instances it starts. Things to know: + +- **Times are emulated**, so a delay is the same at any `--speed`. +- **The delay is one way, on the receiving side.** With it on every instance, a round trip takes twice + the delay more: `--adhoc-delay=50` turns a 2 ms P2P round trip into 102 ms (pspautotests + `net/adhoc/pair/timing`). +- **Runs repeat.** Jitter and loss come from random numbers seeded per instance when the game starts ad + hoc, so with the instances in step (`--sync-file`) the same datagrams are lost each run. +- **PDP** (including matching and game mode): a datagram arrives at the next emulated millisecond after + it's due, and goes into the socket's buffer then, where it's dropped if it doesn't fit, as on a PSP. +- **PTP** is held on the sending side instead: a send is accepted at once and goes out when due, in + order, which with the same options on every instance gives the same round trip. Nothing is lost on a + connection: a "lost" send goes out one retransmit interval (the one given to PtpOpen) later. A flush + waits for what's held, and GetPtpStat counts it as not yet sent. +- The ad hoc server's own messages (logins, groups, peer lists) aren't delayed. + ## Measuring - `stats` reads each instance's frame rate (`gpu.stats.get`). If vblanks/s is below 60, emulation itself diff --git a/libretro/Makefile.common b/libretro/Makefile.common index 67e21b0b08..02bb845a59 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -831,6 +831,7 @@ SOURCES_CXX += \ $(COREDIR)/HLE/NetAdhocCommon.cpp \ $(COREDIR)/HLE/AdhocTransportP2P.cpp \ $(COREDIR)/HLE/AdhocTransportRelay.cpp \ + $(COREDIR)/HLE/AdhocTransportNetSim.cpp \ $(COREDIR)/HLE/sceNetAdhoc.cpp \ $(COREDIR)/HLE/sceNetAdhocMatching.cpp \ $(COREDIR)/HLE/sceNetApctl.cpp \