Netplay: Set up enet connections

This commit is contained in:
Stenzek 2023-05-07 15:00:46 +10:00
parent d6512dc8bc
commit 785b36ce5f
5 changed files with 289 additions and 23 deletions

View File

@ -46,7 +46,8 @@
<ClCompile>
<WarningLevel>TurnOffAllWarnings</WarningLevel>
<PreprocessorDefinitions>_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>$(ProjectDir)src;$(ProjectDir)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(ProjectDir)src;$(ProjectDir)include</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)dep\enet\include</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<Import Project="..\msvc\vsprops\Targets.props" />

View File

@ -9,7 +9,7 @@
<PreprocessorDefinitions Condition="('$(Platform)'=='x64' Or '$(Platform)'=='ARM' Or '$(Platform)'=='ARM64')">WITH_RECOMPILER=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="('$(Platform)'=='x64' Or '$(Platform)'=='ARM64')">WITH_MMAP_FASTMEM=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>$(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)</AdditionalIncludeDirectories>
<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)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories Condition="'$(Platform)'!='ARM64'">$(SolutionDir)dep\rainterface;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories Condition="'$(Platform)'=='x64'">$(SolutionDir)dep\xbyak\xbyak;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
@ -19,7 +19,7 @@
<ItemDefinitionGroup>
<Lib>
<AdditionalDependencies>$(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)</AdditionalDependencies>
<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)</AdditionalDependencies>
<AdditionalDependencies Condition="'$(Platform)'!='ARM64'">$(RootBuildDir)rainterface\rainterface.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies Condition="'$(Platform)'=='ARM64'">$(RootBuildDir)vixl\vixl.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Lib>

View File

@ -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 <bitset>
#include <gsl/span>
#include <deque>
#include <xxhash.h>
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<const ENetAddress> 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<ENetPeer*, MAX_PLAYERS> 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<u32>(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<const ENetAddress> 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<ENetAddress, MAX_PLAYERS> 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<const ENetAddress>(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)

View File

@ -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,

View File

@ -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<float>(progress_value) / static_cast<float>(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();