diff --git a/dep/ggpo-x/ggpo-x.vcxproj b/dep/ggpo-x/ggpo-x.vcxproj
index cf78e9bdf..499cf85ac 100644
--- a/dep/ggpo-x/ggpo-x.vcxproj
+++ b/dep/ggpo-x/ggpo-x.vcxproj
@@ -46,7 +46,8 @@
TurnOffAllWarnings
_WINDOWS;%(PreprocessorDefinitions)
- $(ProjectDir)src;$(ProjectDir)include;%(AdditionalIncludeDirectories)
+ %(AdditionalIncludeDirectories);$(ProjectDir)src;$(ProjectDir)include
+ %(AdditionalIncludeDirectories);$(SolutionDir)dep\enet\include
diff --git a/src/core/core.props b/src/core/core.props
index 6f6436108..1a7fe47d3 100644
--- a/src/core/core.props
+++ b/src/core/core.props
@@ -9,7 +9,7 @@
WITH_RECOMPILER=1;%(PreprocessorDefinitions)
WITH_MMAP_FASTMEM=1;%(PreprocessorDefinitions)
- $(SolutionDir)dep\ggpo-x\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\enet\include;$(SolutionDir)dep\ggpo-x\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
$(SolutionDir)dep\rainterface;%(AdditionalIncludeDirectories)
$(SolutionDir)dep\xbyak\xbyak;%(AdditionalIncludeDirectories)
@@ -19,7 +19,7 @@
- $(RootBuildDir)ggpo-x\ggpo-x.lib;$(RootBuildDir)tinyxml2\tinyxml2.lib;$(RootBuildDir)rcheevos\rcheevos.lib;$(RootBuildDir)imgui\imgui.lib;$(RootBuildDir)stb\stb.lib;$(RootBuildDir)xxhash\xxhash.lib;$(RootBuildDir)zlib\zlib.lib;$(RootBuildDir)util\util.lib;$(RootBuildDir)common\common.lib;%(AdditionalDependencies)
+ $(RootBuildDir)ggpo-x\ggpo-x.lib;$(RootBuildDir)enet\enet.lib;$(RootBuildDir)tinyxml2\tinyxml2.lib;$(RootBuildDir)rcheevos\rcheevos.lib;$(RootBuildDir)imgui\imgui.lib;$(RootBuildDir)stb\stb.lib;$(RootBuildDir)xxhash\xxhash.lib;$(RootBuildDir)zlib\zlib.lib;$(RootBuildDir)util\util.lib;$(RootBuildDir)common\common.lib;%(AdditionalDependencies)
$(RootBuildDir)rainterface\rainterface.lib;%(AdditionalDependencies)
$(RootBuildDir)vixl\vixl.lib;%(AdditionalDependencies)
diff --git a/src/core/netplay.cpp b/src/core/netplay.cpp
index fe1497d1a..f0641834e 100644
--- a/src/core/netplay.cpp
+++ b/src/core/netplay.cpp
@@ -7,12 +7,14 @@
#include "common/timer.h"
#include "digital_controller.h"
#include "ggponet.h"
+#include "enet/enet.h"
#include "host.h"
#include "host_settings.h"
#include "pad.h"
#include "spu.h"
#include "system.h"
#include
+#include
#include
#include
Log_SetChannel(Netplay);
@@ -30,6 +32,8 @@ struct Input
u32 button_data;
};
+static bool InitializeEnet();
+
static bool NpAdvFrameCb(void* ctx, int flags);
static bool NpSaveFrameCb(void* ctx, unsigned char** buffer, int* len, int* checksum, int frame);
static bool NpLoadFrameCb(void* ctx, unsigned char* buffer, int len, int rb_frames, int frame_to_load);
@@ -44,8 +48,19 @@ static void SetInputs(Input inputs[2]);
static void SetSettings();
+static bool CreateSystem(std::string game_path);
+
+// ENet
+static void ShutdownEnetHost();
+static s32 GetFreePlayerId();
+static s32 GetPlayerIdForPeer(const ENetPeer* peer);
+static bool ConnectToLowerPeers(gsl::span peer_addresses);
+static bool WaitForPeerConnections();
+static void HandleEnetEvent(const ENetEvent* event);
+static void PollEnet(Common::Timer::Value until_time);
+
// l = local, r = remote
-static s32 Start(s32 lhandle, u16 lport, std::string& raddr, u16 rport, s32 ldelay, u32 pred);
+static s32 Start(s32 lhandle, u16 lport, const std::string& raddr, u16 rport, s32 ldelay, u32 pred, std::string game_path);
static void AdvanceFrame();
static void RunFrame();
@@ -62,13 +77,18 @@ static void Throttle();
// Desync Detection
static void GenerateChecksumForFrame(int* checksum, int frame, unsigned char* buffer, int buffer_size);
static void GenerateDesyncReport(s32 desync_frame);
+
//////////////////////////////////////////////////////////////////////////
// Variables
//////////////////////////////////////////////////////////////////////////
static MemorySettingsInterface s_settings_overlay;
-static std::string s_game_path;
+
+/// Enet
+static ENetHost* s_enet_host;
+static std::array s_enet_peers;
+static s32 s_player_id;
static GGPOPlayerHandle s_local_handle = GGPO_INVALID_HANDLE;
static GGPONetworkStats s_last_net_stats{};
@@ -88,11 +108,260 @@ static s32 s_next_timesync_recovery_frame = -1;
// Netplay Impl
-s32 Netplay::Start(s32 lhandle, u16 lport, std::string& raddr, u16 rport, s32 ldelay, u32 pred)
+bool Netplay::CreateSystem(std::string game_path)
+{
+ // close system if its already running
+ if (System::IsValid())
+ System::ShutdownSystem(false);
+
+ // fast boot the selected game and wait for the other player
+ auto param = SystemBootParameters(std::move(game_path));
+ param.override_fast_boot = true;
+ return System::BootSystem(param);
+}
+
+bool Netplay::InitializeEnet()
+{
+ static bool enet_initialized = false;
+ int rc;
+ if (!enet_initialized && (rc = enet_initialize()) != 0)
+ {
+ Log_ErrorPrintf("enet_initialize() returned %d", rc);
+ return false;
+ }
+
+ std::atexit(enet_deinitialize);
+ enet_initialized = true;
+ return true;
+}
+
+void Netplay::ShutdownEnetHost()
+{
+ Log_DevPrint("Disconnecting all peers");
+
+ // forcefully disconnect all peers
+ // TODO: do we want to send disconnect requests and wait a bit?
+ for (u32 i = 0; i < MAX_PLAYERS; i++)
+ {
+ if (s_enet_peers[i])
+ {
+ enet_peer_reset(s_enet_peers[i]);
+ s_enet_peers[i] = nullptr;
+ }
+ }
+
+ enet_host_destroy(s_enet_host);
+ s_enet_host = nullptr;
+}
+
+s32 Netplay::GetPlayerIdForPeer(const ENetPeer* peer)
+{
+ for (s32 i = 0; i < MAX_PLAYERS; i++)
+ {
+ if (s_enet_peers[i] == peer)
+ return i;
+ }
+
+ return -1;
+}
+
+s32 Netplay::GetFreePlayerId()
+{
+ for (s32 i = 0; i < MAX_PLAYERS; i++)
+ {
+ if (i != s_player_id && !s_enet_peers[i])
+ return i;
+ }
+
+ return -1;
+}
+
+void Netplay::HandleEnetEvent(const ENetEvent* event)
+{
+ switch (event->type)
+ {
+ case ENET_EVENT_TYPE_CONNECT:
+ {
+ // skip when it's one we set up ourselves, we're handling it in ConnectToLowerPeers().
+ if (GetPlayerIdForPeer(event->peer) >= 0)
+ return;
+
+ // TODO: the player ID should either come from the packet (for the non-first player),
+ // or be auto-assigned as below, for the connection to the first host
+ const s32 new_player_id = GetFreePlayerId();
+ Log_DevPrintf("Enet connect event: New client with id %d", new_player_id);
+
+ if (new_player_id < 0)
+ {
+ Log_ErrorPrintf("No free slots, disconnecting client");
+ enet_peer_disconnect(event->peer, 1);
+ return;
+ }
+
+ s_enet_peers[new_player_id] = event->peer;
+ }
+ break;
+
+ case ENET_EVENT_TYPE_DISCONNECT:
+ {
+ const s32 player_id = GetPlayerIdForPeer(event->peer);
+ if (player_id < 0)
+ return;
+
+ Log_WarningPrintf("ENet player %d disconnected", player_id);
+ s_enet_peers[player_id] = nullptr;
+ }
+ break;
+
+ default:
+ {
+ Log_WarningPrintf("Unhandled enet event %d", event->type);
+ }
+ break;
+ }
+}
+
+void Netplay::PollEnet(Common::Timer::Value until_time)
+{
+ ENetEvent event;
+
+ u64 current_time = Common::Timer::GetCurrentValue();
+
+ for (;;)
+ {
+ const u32 enet_timeout = (current_time >= until_time) ?
+ 0 :
+ static_cast(Common::Timer::ConvertValueToMilliseconds(until_time - current_time));
+ const int res = enet_host_service(s_enet_host, &event, enet_timeout);
+ if (res > 0)
+ {
+ HandleEnetEvent(&event);
+
+ // make sure we get all events
+ current_time = Common::Timer::GetCurrentValue();
+ continue;
+ }
+
+ // exit once we're nonblocking
+ current_time = Common::Timer::GetCurrentValue();
+ if (enet_timeout == 0 || current_time >= until_time)
+ break;
+ }
+}
+
+bool Netplay::ConnectToLowerPeers(gsl::span peer_addresses)
+{
+ for (size_t i = 0; i < peer_addresses.size(); i++)
+ {
+ char ipstr[32];
+ if (enet_address_get_host_ip(&peer_addresses[i], ipstr, std::size(ipstr)) != 0)
+ ipstr[0] = 0;
+ Log_DevPrintf("Starting connection to peer %u at %s:%u", i, ipstr, peer_addresses[i].port);
+
+ DebugAssert(i != s_player_id);
+ s_enet_peers[i] = enet_host_connect(s_enet_host, &peer_addresses[i], NUM_ENET_CHANNELS, 0);
+ if (!s_enet_peers[i])
+ {
+ Log_ErrorPrintf("enet_host_connect() for peer %u failed", i);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool Netplay::WaitForPeerConnections()
+{
+ static constexpr float MAX_CONNECT_TIME = 30.0f;
+ Common::Timer timeout;
+
+ const u32 clients_to_connect = MAX_PLAYERS - 1;
+
+ for (;;)
+ {
+ // TODO: Handle early shutdown/cancel request.
+ u32 num_connected_peers = 0;
+ for (s32 i = 0; i < MAX_PLAYERS; i++)
+ {
+ if (i != s_player_id && s_enet_peers[i] && s_enet_peers[i]->state == ENET_PEER_STATE_CONNECTED)
+ num_connected_peers++;
+ }
+ if (num_connected_peers == clients_to_connect)
+ break;
+
+ if (timeout.GetTimeSeconds() >= MAX_CONNECT_TIME)
+ {
+ Log_ErrorPrintf("Peer connection timeout");
+ return false;
+ }
+
+ Host::PumpMessagesOnCPUThread();
+ Host::DisplayLoadingScreen("Connected to netplay peers", 0, clients_to_connect, num_connected_peers);
+
+ const Common::Timer::Value poll_end_time =
+ Common::Timer::GetCurrentValue() + Common::Timer::ConvertMillisecondsToValue(16);
+ PollEnet(poll_end_time);
+ }
+
+ Log_InfoPrint("Peer connection complete.");
+ return true;
+}
+
+s32 Netplay::Start(s32 lhandle, u16 lport, const std::string& raddr, u16 rport, s32 ldelay, u32 pred, std::string game_path)
{
SetSettings();
+ if (!InitializeEnet())
+ return -1;
+
+ ENetAddress host_address;
+ host_address.host = ENET_HOST_ANY;
+ host_address.port = lport - 10;
+ s_enet_host = enet_host_create(&host_address, MAX_PLAYERS - 1, NUM_ENET_CHANNELS, 0, 0);
+ if (!s_enet_host)
+ {
+ Log_ErrorPrintf("Failed to create enet host.");
+ return -1;
+ }
+
+ // Connect to any lower-ID'ed hosts.
+ // Eventually we'll assign these IDs as players connect, and everyone not starting it will get their ID sent back
+ s_player_id = lhandle - 1;
+
+ std::array peer_addresses;
+ const u32 num_peer_addresses = s_player_id;
+ DebugAssert(num_peer_addresses == 0 || num_peer_addresses == 1);
+ if (num_peer_addresses == 1)
+ {
+ // TODO: rewrite this when we support more players
+ const u32 other_player_id = (lhandle == 1) ? 1 : 0;
+ if (enet_address_set_host_ip(&peer_addresses[other_player_id], raddr.c_str()) != 0)
+ {
+ Log_ErrorPrintf("Failed to parse host: '%s'", raddr.c_str());
+ ShutdownEnetHost();
+ return -1;
+ }
+
+ peer_addresses[other_player_id].port = rport - 10;
+ }
+
+ // Create system.
+ if (!CreateSystem(std::move(game_path)))
+ {
+ Log_ErrorPrintf("Failed to create system.");
+ ShutdownEnetHost();
+ return -1;
+ }
InitializeFramePacing();
+ // Connect to all peers.
+ if ((num_peer_addresses > 0 &&
+ !ConnectToLowerPeers(gsl::span(peer_addresses).subspan(0, num_peer_addresses))) ||
+ !WaitForPeerConnections())
+ {
+ ShutdownEnetHost();
+ return -1;
+ }
+
/*
TODO: since saving every frame during rollback loses us time to do actual gamestate iterations it might be better to
hijack the update / save / load cycle to only save every confirmed frame only saving when actually needed.
@@ -261,6 +530,9 @@ void Netplay::Throttle()
// Poll network.
ggpo_poll_network(s_ggpo);
+ // TODO: make better, we can tell this function to stall until the next frame
+ PollEnet(0);
+
current_time = Common::Timer::GetCurrentValue();
if (current_time >= s_next_frame_time)
break;
@@ -414,13 +686,15 @@ void Netplay::StartNetplaySession(s32 local_handle, u16 local_port, std::string&
// dont want to start a session when theres already one going on.
if (IsActive())
return;
- // set game path for later loading during the begin game callback
- s_game_path = std::move(game_path);
+
// create session
- int result = Netplay::Start(local_handle, local_port, remote_addr, remote_port, input_delay, MAX_ROLLBACK_FRAMES);
+ int result = Netplay::Start(local_handle, local_port, remote_addr, remote_port, input_delay, MAX_ROLLBACK_FRAMES, std::move(game_path));
// notify that the session failed
if (result != GGPO_OK)
+ {
Log_ErrorPrintf("Failed to Create Netplay Session! Error: %d", result);
+ System::ShutdownSystem(false);
+ }
else
{
// Load savestate if available
@@ -465,17 +739,6 @@ void Netplay::ExecuteNetplay()
bool Netplay::NpBeginGameCb(void* ctx, const char* game_name)
{
- // close system if its already running
- if (System::IsValid())
- System::ShutdownSystem(false);
- // fast boot the selected game and wait for the other player
- auto param = SystemBootParameters(s_game_path);
- param.override_fast_boot = true;
- if (!System::BootSystem(param))
- {
- StopNetplaySession();
- return false;
- }
SPU::SetAudioOutputMuted(true);
// Fast Forward to Game Start if needed.
while (System::GetInternalFrameNumber() < 2)
diff --git a/src/core/netplay.h b/src/core/netplay.h
index 5b871a204..f2cb7419f 100644
--- a/src/core/netplay.h
+++ b/src/core/netplay.h
@@ -5,12 +5,14 @@
namespace Netplay {
-enum : u32
+enum : s32
{
// Maximum number of emulated controllers.
MAX_PLAYERS = 2,
// Maximum netplay prediction frames
MAX_ROLLBACK_FRAMES = 8,
+
+ NUM_ENET_CHANNELS = 1,
};
void StartNetplaySession(s32 local_handle, u16 local_port, std::string& remote_addr, u16 remote_port, s32 input_delay,
diff --git a/src/frontend-common/common_host.cpp b/src/frontend-common/common_host.cpp
index 0b6e1d091..7aa5231ec 100644
--- a/src/frontend-common/common_host.cpp
+++ b/src/frontend-common/common_host.cpp
@@ -466,14 +466,14 @@ void Host::DisplayLoadingScreen(const char* message, int progress_min /*= -1*/,
ImGui::Text("%s: %d/%d", message, progress_value, progress_max);
ImGui::ProgressBar(static_cast(progress_value) / static_cast(progress_max - progress_min),
ImVec2(-1.0f, 0.0f), "");
- Log_InfoPrintf("%s: %d/%d", message, progress_value, progress_max);
+ Log_DebugPrintf("%s: %d/%d", message, progress_value, progress_max);
}
else
{
const ImVec2 text_size(ImGui::CalcTextSize(message));
ImGui::SetCursorPosX((width - text_size.x) / 2.0f);
ImGui::TextUnformatted(message);
- Log_InfoPrintf("%s", message);
+ Log_DebugPrintf("%s", message);
}
}
ImGui::End();