From 06fb698fafe5ebef4ac94d1330feb36009be1f84 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Mon, 2 Jan 2023 16:12:05 +0100 Subject: [PATCH] NetPlay: Add support for user-triggered events. --- Source/Core/Core/Core.cpp | 3 + Source/Core/Core/HW/ProcessorInterface.cpp | 9 + Source/Core/Core/HW/ProcessorInterface.h | 1 + Source/Core/Core/NetPlayClient.cpp | 190 +++++++++++++++++++++ Source/Core/Core/NetPlayClient.h | 6 + Source/Core/Core/NetPlayProto.h | 10 ++ Source/Core/Core/NetPlayServer.cpp | 42 +++++ Source/Core/Core/NetPlayServer.h | 2 + Source/Core/DolphinQt/MainWindow.cpp | 2 +- 9 files changed, 264 insertions(+), 1 deletion(-) diff --git a/Source/Core/Core/Core.cpp b/Source/Core/Core/Core.cpp index 8390bd44b6..b9efc8b6e0 100644 --- a/Source/Core/Core/Core.cpp +++ b/Source/Core/Core/Core.cpp @@ -573,6 +573,9 @@ static void EmuThread(std::unique_ptr 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; diff --git a/Source/Core/Core/HW/ProcessorInterface.cpp b/Source/Core/Core/HW/ProcessorInterface.cpp index cea84a4ab4..7516be3161 100644 --- a/Source/Core/Core/HW/ProcessorInterface.cpp +++ b/Source/Core/Core/HW/ProcessorInterface.cpp @@ -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()) diff --git a/Source/Core/Core/HW/ProcessorInterface.h b/Source/Core/Core/HW/ProcessorInterface.h index 6387634843..7e2fbb0bc3 100644 --- a/Source/Core/Core/HW/ProcessorInterface.h +++ b/Source/Core/Core/HW/ProcessorInterface.h @@ -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; diff --git a/Source/Core/Core/NetPlayClient.cpp b/Source/Core/Core/NetPlayClient.cpp index 55dea9b3d9..a00ea41435 100644 --- a/Source/Core/Core/NetPlayClient.cpp +++ b/Source/Core/Core/NetPlayClient.cpp @@ -4,6 +4,8 @@ #include "Core/NetPlayClient.h" #include +#include +#include #include #include #include @@ -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 map; + ExternalEventID event_id = ExternalEventID::None; +}; + +static std::mutex s_event_sync_mutex; +static std::map 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(uid)].event_id = eeid; + } + + auto& core_timing = Core::System::GetInstance().GetCoreTiming(); + core_timing.ScheduleExternalEvent(static_cast(timepoint), event_type_sync_time_for_ext_event, + static_cast(uid), static_cast(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(uid)]; + { + std::lock_guard lk2(event_data.mutex); + event_data.map[pid] = static_cast(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(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 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(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 diff --git a/Source/Core/Core/NetPlayClient.h b/Source/Core/Core/NetPlayClient.h index 7ab716aef7..1ce54307fe 100644 --- a/Source/Core/Core/NetPlayClient.h +++ b/Source/Core/Core/NetPlayClient.h @@ -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& entries); unsigned int NetPlay_GetLocalWiimoteForSlot(unsigned int slot); + +void NetPlay_RegisterEvents(); } // namespace NetPlay diff --git a/Source/Core/Core/NetPlayProto.h b/Source/Core/Core/NetPlayProto.h index a20aee39ce..217a6a63f7 100644 --- a/Source/Core/Core/NetPlayProto.h +++ b/Source/Core/Core/NetPlayProto.h @@ -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); diff --git a/Source/Core/Core/NetPlayServer.cpp b/Source/Core/Core/NetPlayServer.cpp index a939a15681..f2af6e7ad1 100644 --- a/Source/Core/Core/NetPlayServer.cpp +++ b/Source/Core/Core/NetPlayServer.cpp @@ -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(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(mid), player.pid); diff --git a/Source/Core/Core/NetPlayServer.h b/Source/Core/Core/NetPlayServer.h index 621b7cfe40..4894be1094 100644 --- a/Source/Core/Core/NetPlayServer.h +++ b/Source/Core/Core/NetPlayServer.h @@ -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 diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index bf7e7e776f..9d9427e1a3 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -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()