NetPlay: Add support for user-triggered events.

This commit is contained in:
Admiral H. Curtiss 2023-01-02 16:12:05 +01:00
commit 06fb698faf
No known key found for this signature in database
GPG key ID: F051B4C4044F33FB
9 changed files with 264 additions and 1 deletions

View file

@ -573,6 +573,9 @@ static void EmuThread(std::unique_ptr<BootParameters> boot, WindowSystemInfo wsi
HW::Init(system,
NetPlay::IsNetPlayRunning() ? &(boot_session_data.GetNetplaySettings()->sram) : nullptr);
if (NetPlay::IsNetPlayRunning())
NetPlay::NetPlay_RegisterEvents();
Common::ScopeGuard hw_guard{[&system] {
// We must set up this flag before executing HW::Shutdown()
s_hardware_initialized = false;

View file

@ -17,6 +17,7 @@
#include "Core/HW/SystemTimers.h"
#include "Core/IOS/IOS.h"
#include "Core/IOS/STM/STM.h"
#include "Core/NetPlayClient.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/System.h"
#include "VideoCommon/AsyncRequests.h"
@ -266,6 +267,14 @@ void ProcessorInterfaceManager::ResetButton_Tap()
false, CoreTiming::FromThread::ANY);
}
void ProcessorInterfaceManager::ResetButton_Tap_FromUser()
{
if (NetPlay::IsNetPlayRunning())
NetPlay::ScheduleResetButtonTap();
else
ResetButton_Tap();
}
void ProcessorInterfaceManager::PowerButton_Tap()
{
if (!Core::IsRunning())

View file

@ -82,6 +82,7 @@ public:
// Thread-safe func which sets and clears reset button state automagically
void ResetButton_Tap();
void ResetButton_Tap_FromUser();
void PowerButton_Tap();
u32 m_interrupt_cause = 0;

View file

@ -4,6 +4,8 @@
#include "Core/NetPlayClient.h"
#include <algorithm>
#include <cmath>
#include <condition_variable>
#include <cstddef>
#include <cstring>
#include <fstream>
@ -49,6 +51,7 @@
#include "Core/HW/GBAPad.h"
#include "Core/HW/GCMemcard/GCMemcard.h"
#include "Core/HW/GCPad.h"
#include "Core/HW/ProcessorInterface.h"
#include "Core/HW/SI/SI.h"
#include "Core/HW/SI/SI_Device.h"
#include "Core/HW/SI/SI_DeviceGCController.h"
@ -84,6 +87,19 @@ static std::mutex crit_netplay_client;
static NetPlayClient* netplay_client = nullptr;
static bool s_si_poll_batching = false;
struct ExternalEventSyncData
{
std::mutex mutex;
std::condition_variable cv;
std::map<PlayerId, u64> map;
ExternalEventID event_id = ExternalEventID::None;
};
static std::mutex s_event_sync_mutex;
static std::map<u64, ExternalEventSyncData> s_event_sync_map;
static CoreTiming::EventType* event_type_sync_time_for_ext_event = nullptr;
static CoreTiming::EventType* event_type_ext_event = nullptr;
// called from ---GUI--- thread
NetPlayClient::~NetPlayClient()
{
@ -436,6 +452,14 @@ void NetPlayClient::OnData(sf::Packet& packet)
OnPowerButton();
break;
case MessageID::ScheduleExternalEvent:
OnScheduleExternalEvent(packet);
break;
case MessageID::SyncTimepointForExternalEvent:
OnSyncTimepointForExternalEvent(packet);
break;
case MessageID::Ping:
OnPing(packet);
break;
@ -952,6 +976,61 @@ void NetPlayClient::OnPowerButton()
m_dialog->OnMsgPowerButton();
}
void NetPlayClient::OnScheduleExternalEvent(sf::Packet& packet)
{
// A user (could be ourselves, too) has requested the scheduling of an external event.
ExternalEventID eeid;
packet >> eeid;
sf::Uint64 uid;
packet >> uid;
sf::Uint64 timepoint;
packet >> timepoint;
DEBUG_LOG_FMT(NETPLAY, "OnScheduleExternalEvent(): eeid {}, timepoint {}, uid {}", u8(eeid),
timepoint, uid);
// The 'uid' is unique per netplay session and is both used to provide an absolute ordering in
// case two events get assigned to the same timepoint, and also to keep track (in
// s_event_sync_map) of what event-related packet belongs to what event if multiple are in flight
// at the same time.
{
std::lock_guard lk(s_event_sync_mutex);
s_event_sync_map[static_cast<u64>(uid)].event_id = eeid;
}
auto& core_timing = Core::System::GetInstance().GetCoreTiming();
core_timing.ScheduleExternalEvent(static_cast<u64>(timepoint), event_type_sync_time_for_ext_event,
static_cast<u64>(uid), static_cast<u64>(uid));
}
void NetPlayClient::OnSyncTimepointForExternalEvent(sf::Packet& packet)
{
// A user (not ourselves) is currently waiting to execute an external event and has sent us their
// current timepoint to determine what the earliest timepoint the event can execute is.
PlayerId pid;
packet >> pid;
sf::Uint64 uid;
packet >> uid;
sf::Uint64 timepoint;
packet >> timepoint;
DEBUG_LOG_FMT(NETPLAY, "OnSyncTimepointForExternalEvent(): player id {}, timepoint {}, uid {}",
u8(pid), timepoint, uid);
{
std::lock_guard lk(s_event_sync_mutex);
auto& event_data = s_event_sync_map[static_cast<u64>(uid)];
{
std::lock_guard lk2(event_data.mutex);
event_data.map[pid] = static_cast<u64>(timepoint);
}
event_data.cv.notify_all();
}
}
void NetPlayClient::OnPing(sf::Packet& packet)
{
u32 ping_key = 0;
@ -2359,6 +2438,14 @@ void NetPlayClient::RequestStopGame()
SendStopGamePacket();
}
void NetPlayClient::ScheduleExternalEvent(ExternalEventID id)
{
sf::Packet packet;
packet << MessageID::ScheduleExternalEvent;
packet << id;
SendAsync(std::move(packet));
}
void NetPlayClient::SendPowerButtonEvent()
{
sf::Packet packet;
@ -2685,6 +2772,12 @@ void SetSIPollBatching(bool state)
s_si_poll_batching = state;
}
void ScheduleResetButtonTap()
{
ASSERT(IsNetPlayRunning());
netplay_client->ScheduleExternalEvent(ExternalEventID::ResetButton);
}
void SendPowerButtonEvent()
{
ASSERT(IsNetPlayRunning());
@ -2765,6 +2858,103 @@ void NetPlay_Disable()
std::lock_guard lk(crit_netplay_client);
netplay_client = nullptr;
}
static void NetPlayExtEvent(Core::System& system, u64 userdata, s64 cyclesLate)
{
ExternalEventID eeid = static_cast<ExternalEventID>(userdata);
switch (eeid)
{
case ExternalEventID::ResetButton:
system.GetProcessorInterface().ResetButton_Tap();
break;
default:
WARN_LOG_FMT(NETPLAY, "NetPlayExtEvent: Invalid event type. ({})", userdata);
break;
}
}
void NetPlayClient::SendTimepointForNetPlayEvent(u64 timepoint, u64 uid)
{
sf::Packet packet;
packet << MessageID::SyncTimepointForExternalEvent;
packet << sf::Uint64(uid);
packet << sf::Uint64(timepoint);
SendAsync(std::move(packet));
}
static void NetPlaySyncEvent(Core::System& system, u64 userdata, s64 cyclesLate)
{
// An external event should be executed, but we're not sure of a valid timepoint for it yet. To
// figure this out, we now send our current timepoint to all other players (as they do the same),
// then once we know all timepoints we execute the event after the last timepoint of any player;
// this ensures that every player can run it at the same timepoint and thus stay deterministic
// with eachother.
auto& core_timing = system.GetCoreTiming();
const u64 timepoint = core_timing.GetTicks();
ExternalEventSyncData* ptr_event_data;
{
std::lock_guard lk(s_event_sync_mutex);
ptr_event_data = &s_event_sync_map[userdata];
}
auto& event_data = *ptr_event_data;
{
std::lock_guard lk(event_data.mutex);
event_data.map[netplay_client->GetLocalPlayerId()] = timepoint;
}
netplay_client->SendTimepointForNetPlayEvent(timepoint, userdata);
const size_t num_players = netplay_client->GetPlayers().size();
bool do_schedule = false;
u64 latest_timepoint = 0;
{
std::unique_lock<std::mutex> lock(event_data.mutex);
if (event_data.map.size() != num_players)
{
DEBUG_LOG_FMT(NETPLAY, "NetPlaySyncEvent({}): Waiting for timepoints.", userdata);
event_data.cv.wait_for(lock, std::chrono::seconds(5), [&event_data, num_players] {
return event_data.map.size() == num_players;
});
}
if (event_data.map.size() != num_players)
{
WARN_LOG_FMT(NETPLAY, "NetPlaySyncEvent({}): Timed out waiting for timepoints.", userdata);
}
else
{
for (const auto& it : event_data.map)
{
DEBUG_LOG_FMT(NETPLAY, "NetPlaySyncEvent({}): Timepoint {} from player {}.", userdata,
it.second, u8(it.first));
latest_timepoint = std::max(it.second, latest_timepoint);
}
do_schedule = true;
}
}
const ExternalEventID eeid = event_data.event_id;
{
std::lock_guard lk(s_event_sync_mutex);
s_event_sync_map.erase(userdata);
}
if (do_schedule)
{
core_timing.ScheduleExternalEvent(latest_timepoint + 1, event_type_ext_event,
static_cast<u64>(eeid), userdata);
}
}
void NetPlay_RegisterEvents()
{
auto& core_timing = Core::System::GetInstance().GetCoreTiming();
event_type_ext_event = core_timing.RegisterEvent("NetPlayExtEvent", NetPlayExtEvent);
event_type_sync_time_for_ext_event =
core_timing.RegisterEvent("NetPlaySyncEvent", NetPlaySyncEvent);
}
} // namespace NetPlay
// stuff hacked into dolphin

View file

@ -130,6 +130,8 @@ public:
bool ChangeGame(const std::string& game);
void SendChatMessage(const std::string& msg);
void RequestStopGame();
void SendTimepointForNetPlayEvent(u64 timepoint, u64 uid);
void ScheduleExternalEvent(ExternalEventID id);
void SendPowerButtonEvent();
void RequestGolfControl(PlayerId pid);
void RequestGolfControl();
@ -297,6 +299,8 @@ private:
void OnStartGame(sf::Packet& packet);
void OnStopGame(sf::Packet& packet);
void OnPowerButton();
void OnScheduleExternalEvent(sf::Packet& packet);
void OnSyncTimepointForExternalEvent(sf::Packet& packet);
void OnPing(sf::Packet& packet);
void OnPlayerPingData(sf::Packet& packet);
void OnDesyncDetected(sf::Packet& packet);
@ -356,4 +360,6 @@ void NetPlay_Enable(NetPlayClient* const np);
void NetPlay_Disable();
bool NetPlay_GetWiimoteData(const std::span<NetPlayClient::WiimoteDataBatchEntry>& entries);
unsigned int NetPlay_GetLocalWiimoteForSlot(unsigned int slot);
void NetPlay_RegisterEvents();
} // namespace NetPlay

View file

@ -169,6 +169,9 @@ enum class MessageID : u8
HostInputAuthority = 0xA6,
PowerButton = 0xA7,
ScheduleExternalEvent = 0xA8,
SyncTimepointForExternalEvent = 0xA9,
TimeBase = 0xB0,
DesyncDetected = 0xB1,
@ -220,6 +223,12 @@ enum class SyncCodeID : u8
Failure = 6,
};
enum class ExternalEventID : u8
{
None = 0,
ResetButton = 1,
};
constexpr u32 MAX_NAME_LENGTH = 30;
constexpr size_t CHUNKED_DATA_UNIT_SIZE = 16384;
constexpr u32 MAX_ENET_MTU = 1392; // see https://github.com/lsalzman/enet/issues/132
@ -257,6 +266,7 @@ std::string GetPlayerMappingString(PlayerId pid, const PadMappingArray& pad_map,
const PadMappingArray& wiimote_map);
bool IsNetPlayRunning();
void SetSIPollBatching(bool state);
void ScheduleResetButtonTap();
void SendPowerButtonEvent();
std::string GetGBASavePath(int pad_num);
PadDetails GetPadDetails(int pad_num);

View file

@ -62,6 +62,7 @@
#include "Core/NetPlayClient.h" //for NetPlayUI
#include "Core/NetPlayCommon.h"
#include "Core/SyncIdentifier.h"
#include "Core/System.h"
#include "DiscIO/Enums.h"
#include "DiscIO/RiivolutionPatcher.h"
@ -1243,6 +1244,47 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, Client& player)
}
break;
case MessageID::ScheduleExternalEvent:
{
ExternalEventID eeid;
packet >> eeid;
sf::Packet spac;
spac << MessageID::ScheduleExternalEvent;
spac << eeid;
const u64 uid = m_external_event_uid_counter++;
spac << sf::Uint64(uid);
auto& core_timing = Core::System::GetInstance().GetCoreTiming();
// We schedule the event in the future so that it's likely it will reach all users in time.
// There's a syncing logic in place to ensure the event executes at the same timepoint
// everywhere even if this packet is a bit too late, but the sync can get tripped up (and
// subsequently time out) if there's too large of a time distance between multiple players
// executing the sync function, because one of them may be waiting for controller input while
// another is waiting for the event timepoint sync.
const u64 target_timepoint =
static_cast<u64>(core_timing.GetGlobals().global_timer) + SystemTimers::GetTicksPerSecond();
spac << sf::Uint64(target_timepoint);
SendToClients(spac);
}
break;
case MessageID::SyncTimepointForExternalEvent:
{
sf::Uint64 uid;
packet >> uid;
sf::Uint64 timepoint;
packet >> timepoint;
sf::Packet spac;
spac << MessageID::SyncTimepointForExternalEvent;
spac << player.pid;
spac << uid;
spac << timepoint;
SendToClients(spac, player.pid);
}
break;
default:
PanicAlertFmtT("Unknown message with id:{0} received from player:{1} Kicking player!",
static_cast<u8>(mid), player.pid);

View file

@ -214,5 +214,7 @@ private:
Common::TraversalClient* m_traversal_client = nullptr;
NetPlayUI* m_dialog = nullptr;
NetPlayIndex m_index;
u64 m_external_event_uid_counter = 0;
};
} // namespace NetPlay

View file

@ -1001,7 +1001,7 @@ void MainWindow::Reset()
if (Movie::IsRecordingInput())
Movie::SetReset(true);
auto& system = Core::System::GetInstance();
system.GetProcessorInterface().ResetButton_Tap();
system.GetProcessorInterface().ResetButton_Tap_FromUser();
}
void MainWindow::FrameAdvance()