Merge branch 'AdmiralCurtiss-netplay-events'

This commit is contained in:
Nayla Hanegan 2024-03-03 19:43:53 -05:00
commit fa950acffc
No known key found for this signature in database
GPG key ID: 3075216CED0DB01D
11 changed files with 318 additions and 1 deletions

View file

@ -563,6 +563,9 @@ static void EmuThread(Core::System& system, std::unique_ptr<BootParameters> 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;

View file

@ -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<s64>(timepoint), unique_id, userdata, event_type});
std::push_heap(m_external_event_queue.begin(), m_external_event_queue.end(),
std::greater<Event>());
}
else
{
std::lock_guard lk(m_ts_write_lock);
m_external_pending_queue.Push(
Event{static_cast<s64>(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<Event>());
}
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<Event>());
}
}
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<Event>());
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)

View file

@ -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<Event, false> 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<Event> m_external_event_queue;
Common::SPSCQueue<Event, false> m_external_pending_queue;
float m_last_oc_factor = 0.0f;
s64 m_idled_cycles = 0;

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()
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())

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

@ -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()