mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-13 09:39:33 +01:00
Merge pull request #22867 from hrydgard/adhoc-network-sim
Ad hoc: Options to simulate a slow network
This commit is contained in:
19 files changed
+377
-20
No files matched your search
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -211,6 +211,9 @@ struct CommandLineOptions {
|
||||
std::optional<bool> adhocServer;
|
||||
std::optional<bool> lagSync;
|
||||
std::optional<std::string> nickname;
|
||||
std::optional<int> adhocDelayMs;
|
||||
std::optional<int> adhocJitterMs;
|
||||
std::optional<int> adhocLossPercent;
|
||||
|
||||
std::optional<std::string> screenshotFilename;
|
||||
std::optional<std::string> screenshotFilenameSave;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<std::string> vCustomAdhocServerList;
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
<ClCompile Include="HLE\NetAdhocCommon.cpp" />
|
||||
<ClCompile Include="HLE\AdhocTransportP2P.cpp" />
|
||||
<ClCompile Include="HLE\AdhocTransportRelay.cpp" />
|
||||
<ClCompile Include="HLE\AdhocTransportNetSim.cpp" />
|
||||
<ClCompile Include="HLE\sceNetAdhoc.cpp" />
|
||||
<ClCompile Include="HLE\sceNetAdhocMatching.cpp" />
|
||||
<ClCompile Include="HLE\sceNp.cpp" />
|
||||
|
||||
@@ -533,6 +533,9 @@
|
||||
<Filter>HLE\Libraries</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="HLE\AdhocTransportRelay.cpp">
|
||||
<ClCompile Include="HLE\AdhocTransportNetSim.cpp">
|
||||
<Filter>HLE\Libraries</Filter>
|
||||
</ClCompile>
|
||||
<Filter>HLE\Libraries</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="HLE\sceNetAdhoc.cpp">
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
|
||||
#include "Core/HLE/proAdhoc.h"
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#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<std::chrono::steady_clock> 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<AdhocTransport> 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<u8> &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<u8> data;
|
||||
};
|
||||
struct HeldSend {
|
||||
u64 departureUs;
|
||||
std::vector<u8> data;
|
||||
};
|
||||
|
||||
u64 Delay();
|
||||
bool Lost();
|
||||
u32 HeldBytes(const AdhocSocket *sock) const;
|
||||
void SendHeld(int id, bool all);
|
||||
|
||||
std::unique_ptr<AdhocTransport> inner_;
|
||||
std::map<const AdhocSocket *, std::deque<HeldDatagram>> datagrams_;
|
||||
std::map<const AdhocSocket *, std::deque<HeldSend>> sends_;
|
||||
std::mt19937 random_;
|
||||
};
|
||||
|
||||
AdhocTransport &Transport();
|
||||
@@ -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 <algorithm>
|
||||
#include <cerrno>
|
||||
|
||||
#include "Core/Config.h"
|
||||
#include "Core/CoreTiming.h"
|
||||
#include "Core/HLE/HLE.h"
|
||||
#include "Core/HLE/AdhocTransport.h"
|
||||
|
||||
NetSimTransport::NetSimTransport(std::unique_ptr<AdhocTransport> 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<u8> &data) {
|
||||
std::deque<HeldDatagram> &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<HeldSend> &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<u8>(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<HeldSend> &held = it->second;
|
||||
while (!held.empty() && (all || held.front().departureUs <= now)) {
|
||||
std::vector<u8> &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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<AdhocTransport> 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<RelayTransport>();
|
||||
} else {
|
||||
transport = std::make_unique<P2PTransport>();
|
||||
}
|
||||
if (NetSimTransport::Enabled()) {
|
||||
// The random numbers are the same for a run each time, but not on every instance.
|
||||
transport = std::make_unique<NetSimTransport>(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();
|
||||
|
||||
+15
-4
@@ -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)")
|
||||
|
||||
@@ -539,6 +539,7 @@
|
||||
<ClCompile Include="..\..\Core\HLE\NetAdhocCommon.cpp" />
|
||||
<ClCompile Include="..\..\Core\HLE\AdhocTransportP2P.cpp" />
|
||||
<ClCompile Include="..\..\Core\HLE\AdhocTransportRelay.cpp" />
|
||||
<ClCompile Include="..\..\Core\HLE\AdhocTransportNetSim.cpp" />
|
||||
<ClCompile Include="..\..\Core\HLE\sceNetAdhoc.cpp" />
|
||||
<ClCompile Include="..\..\Core\HLE\sceNetAdhocMatching.cpp" />
|
||||
<ClCompile Include="..\..\Core\HLE\sceNp.cpp" />
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
<ClCompile Include="..\..\Core\HLE\NetAdhocCommon.cpp" />
|
||||
<ClCompile Include="..\..\Core\HLE\AdhocTransportP2P.cpp" />
|
||||
<ClCompile Include="..\..\Core\HLE\AdhocTransportRelay.cpp" />
|
||||
<ClCompile Include="..\..\Core\HLE\AdhocTransportNetSim.cpp" />
|
||||
<ClCompile Include="..\..\Core\HLE\sceNetAdhoc.cpp" />
|
||||
<ClCompile Include="..\..\Core\HLE\sceNetAdhocMatching.cpp" />
|
||||
<ClCompile Include="..\..\Core\HLE\sceNp.cpp" />
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
Reference in new issue
Block a user