diff --git a/Source/Core/Core/Core.cpp b/Source/Core/Core/Core.cpp index c7c392fa0e..bbceb6c0fc 100644 --- a/Source/Core/Core/Core.cpp +++ b/Source/Core/Core/Core.cpp @@ -563,6 +563,9 @@ static void EmuThread(Core::System& system, std::unique_ptr boot 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/CoreTiming.cpp b/Source/Core/Core/CoreTiming.cpp index 627f14cbce..d3dc01e146 100644 --- a/Source/Core/Core/CoreTiming.cpp +++ b/Source/Core/Core/CoreTiming.cpp @@ -212,6 +212,9 @@ void CoreTimingManager::DoState(PointerWrap& p) // The stave state has changed the time, so our previous Throttle targets are invalid. // Especially when global_time goes down; So we create a fake throttle update. ResetThrottle(m_globals.global_timer); + + // Throw away pending external events when loading state, they no longer apply. + m_external_event_queue.clear(); } } @@ -282,6 +285,24 @@ void CoreTimingManager::ScheduleEvent(s64 cycles_into_future, EventType* event_t } } +void CoreTimingManager::ScheduleExternalEvent(u64 timepoint, EventType* event_type, u64 userdata, + u64 unique_id) +{ + if (Core::IsCPUThread()) + { + m_external_event_queue.emplace_back( + Event{static_cast(timepoint), unique_id, userdata, event_type}); + std::push_heap(m_external_event_queue.begin(), m_external_event_queue.end(), + std::greater()); + } + else + { + std::lock_guard lk(m_ts_write_lock); + m_external_pending_queue.Push( + Event{static_cast(timepoint), unique_id, userdata, event_type}); + } +} + void CoreTimingManager::RemoveEvent(EventType* event_type) { auto itr = std::remove_if(m_event_queue.begin(), m_event_queue.end(), @@ -322,6 +343,13 @@ void CoreTimingManager::MoveEvents() m_event_queue.emplace_back(std::move(ev)); std::push_heap(m_event_queue.begin(), m_event_queue.end(), std::greater()); } + + for (Event ev; m_external_pending_queue.Pop(ev);) + { + m_external_event_queue.emplace_back(std::move(ev)); + std::push_heap(m_external_event_queue.begin(), m_external_event_queue.end(), + std::greater()); + } } void CoreTimingManager::Advance() @@ -351,6 +379,16 @@ void CoreTimingManager::Advance() evt.type->callback(m_system, evt.userdata, m_globals.global_timer - evt.time); } + while (!m_external_event_queue.empty() && + m_external_event_queue.front().time <= m_globals.global_timer) + { + Event evt = std::move(m_external_event_queue.front()); + std::pop_heap(m_external_event_queue.begin(), m_external_event_queue.end(), + std::greater()); + m_external_event_queue.pop_back(); + evt.type->callback(m_system, evt.userdata, m_globals.global_timer - evt.time); + } + m_is_global_timer_sane = false; // Still events left (scheduled in the future) diff --git a/Source/Core/Core/CoreTiming.h b/Source/Core/Core/CoreTiming.h index 6c60b74479..d20ab11889 100644 --- a/Source/Core/Core/CoreTiming.h +++ b/Source/Core/Core/CoreTiming.h @@ -104,6 +104,13 @@ public: void ScheduleEvent(s64 cycles_into_future, EventType* event_type, u64 userdata = 0, FromThread from = FromThread::CPU); + // Similar to ScheduleEvent, but enqueues an event in the secondary event queue that does not + // affect timing logic and isn't savestated. Used primarily for handling events in a deterministic + // manner during netplay. Note that 'timepoint' is absolute (instead of ScheduleEvent's relative) + // and that the user should try to provide a 'unique_id' for consistent event ordering if they + // happen to be at the same timepoint. + void ScheduleExternalEvent(u64 timepoint, EventType* event_type, u64 userdata, u64 unique_id); + // We only permit one event of each type in the queue at a time. void RemoveEvent(EventType* event_type); void RemoveAllEvents(EventType* event_type); @@ -172,6 +179,15 @@ private: std::mutex m_ts_write_lock; Common::SPSCQueue m_ts_queue; + // A second event queue that is used for timing 'external' events that are sent by the emulator + // rather than by the emulated game. Netplay uses these for syncing non-controller-button events + // sent by a single client, such as a press of the physical Reset button on the console, or an + // unplugging of a controller. These don't affect timing logic (and thus will not run at a precise + // time, but instead at the first opportunity given by the regular events) and do not get written + // to savestates. + std::vector m_external_event_queue; + Common::SPSCQueue m_external_pending_queue; + float m_last_oc_factor = 0.0f; s64 m_idled_cycles = 0; diff --git a/Source/Core/Core/HW/ProcessorInterface.cpp b/Source/Core/Core/HW/ProcessorInterface.cpp index 679f5d4092..53e3b24389 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() m_event_type_toggle_reset_button, 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 c40b122a70..f6ba91fdf9 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; @@ -2361,6 +2440,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; @@ -2687,6 +2774,12 @@ void SetSIPollBatching(bool state) s_si_poll_batching = state; } +void ScheduleResetButtonTap() +{ + ASSERT(IsNetPlayRunning()); + netplay_client->ScheduleExternalEvent(ExternalEventID::ResetButton); +} + void SendPowerButtonEvent() { ASSERT(IsNetPlayRunning()); @@ -2767,6 +2860,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 4eece1525a..7fc363e847 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 SendActiveGeckoCodes(); void GetActiveGeckoCodes(); @@ -300,6 +302,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); @@ -359,4 +363,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 1634d3689f..0915410298 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 6065c4ab54..4a7b69f0ac 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -1122,7 +1122,7 @@ void MainWindow::Reset() auto& movie = system.GetMovie(); if (movie.IsRecordingInput()) movie.SetReset(true); - system.GetProcessorInterface().ResetButton_Tap(); + system.GetProcessorInterface().ResetButton_Tap_FromUser(); } void MainWindow::FrameAdvance()