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..cd18042e82 100644 --- a/Core/HLE/AdhocTransport.h +++ b/Core/HLE/AdhocTransport.h @@ -19,6 +19,9 @@ #include "Core/HLE/proAdhoc.h" #include +#include +#include +#include #include #include "Core/HLE/sceNetAdhoc.h" @@ -76,6 +79,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; @@ -153,6 +159,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; @@ -189,6 +196,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; @@ -211,4 +219,72 @@ 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 another one, which Transport() puts in while they're on, or while +// it still holds data. Everything it doesn't delay goes straight to the transport inside. +class NetSimTransport : public AdhocTransport { +public: + // Whether the options are on or data is held, with inner the transport in use: if so, Transport() + // returns this one. + bool Wraps(AdhocTransport &inner); + // Drops what's held, and seeds the random numbers, which differ per instance. + void Reset(uint32_t seed); + + void CloseHostSocket(AdhocSocket *sock) override; + void Poll() override; + bool BindServerConnectionToLocalIp() override { return inner_->BindServerConnectionToLocalIp(); } + + 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(); + bool Holding() const; + u32 HeldBytes(const AdhocSocket *sock) const; + void SendHeld(int id, bool all); + + AdhocTransport *inner_ = nullptr; + 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..7de15dc932 --- /dev/null +++ b/Core/HLE/AdhocTransportNetSim.cpp @@ -0,0 +1,200 @@ +// 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" + +static bool NetSimOptionsOn() { + return g_Config.iAdhocSimDelayMs > 0 || g_Config.iAdhocSimJitterMs > 0 || g_Config.iAdhocSimLossPercent > 0; +} + +bool NetSimTransport::Wraps(AdhocTransport &inner) { + inner_ = &inner; + return NetSimOptionsOn() || Holding(); +} + +void NetSimTransport::Reset(uint32_t seed) { + datagrams_.clear(); + sends_.clear(); + random_.seed(seed); +} + +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; +} + +bool NetSimTransport::Holding() const { + for (const auto &it : datagrams_) { + if (!it.second.empty()) { + return true; + } + } + for (const auto &it : sends_) { + if (!it.second.empty()) { + return true; + } + } + return false; +} + +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]; + // With the options off, only what's behind held data waits. + if (!NetSimOptionsOn() && HeldBytes(sock) == 0) { + return inner_->PtpSend(id, data, len, timeout, flag); + } + std::deque &held = sends_[sock]; + u64 delayUs = NetSimOptionsOn() ? Delay() : 0; + if (NetSimOptionsOn() && 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/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 84cc2dbd47..ac5935cb44 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" @@ -381,12 +382,15 @@ int NetAdhocctl_ExitGameMode(); static RelayTransport relayTransport; static P2PTransport p2pTransport; +static NetSimTransport netSimTransport; AdhocTransport &Transport() { - if (serverHasRelay) { - return relayTransport; + AdhocTransport &transport = serverHasRelay ? static_cast(relayTransport) : p2pTransport; + // The network simulation's options wrap whichever is in use. + if (netSimTransport.Wraps(transport)) { + return netSimTransport; } - return p2pTransport; + return transport; } // Forward declarations for the savestate mechanism (the matching is sadly not inside its own section) @@ -1778,6 +1782,8 @@ u32 sceNetAdhocInit(MIPSState *mips) { // Library initialized netAdhocInited = true; isAdhocctlBusy = false; + // The network simulation's random numbers: the same for a run each time, but not on every instance. + netSimTransport.Reset(PPSSPP_ID); // FIXME: It seems official prx is using sceNetAdhocGameModeDeleteMaster in here? NetAdhocGameMode_DeleteMaster(); 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 \