From 938981f89983c9cfd6deb8c7ee300f78bd0ff38a Mon Sep 17 00:00:00 2001 From: HeatXD <45072324+HeatXD@users.noreply.github.com> Date: Sun, 19 Mar 2023 04:30:23 +0100 Subject: [PATCH] Import fork code Netplay Refactor: Qt Redo Netplay Window Added ggpo-x dependency. Qt/Netplay: Redone DirectIP Functionality Integrate Netplay into the Pad Runahead system Qt/Netplay: bring back in all the Netplay features besides the chat window thats still wip. Qt/Netplay Rewire Netplay chat window Qt/Netplay: Add Temporary alert stating this feature is not ready yet Settings: Apply the right settings when netplay is active Qt/Netplay: Fixed crashing causes my missing references in the gamelist also faster loading of the game at start of the session. Netplay: fixed stuttery feeling at high ping due to wrong order of operations including input in time calculations. Qt/Netplay Cleanup and fix stuttering at higher ping. Qt/Netplay Cleanup and fix stuttering at higher ping. --- src/core/netplay.cpp | 265 ++++++++++ src/core/netplay.h | 102 ++++ src/core/pad.cpp | 4 +- src/core/system.cpp | 197 ++++++- src/core/system.h | 16 +- src/duckstation-qt/mainwindow.cpp | 41 +- src/duckstation-qt/mainwindow.h | 6 + src/duckstation-qt/mainwindow.ui | 26 +- src/duckstation-qt/netplaywidget.cpp | 210 ++++++++ src/duckstation-qt/netplaywidget.h | 34 ++ src/duckstation-qt/netplaywidget.ui | 710 ++++++++++++++++++++++++++ src/duckstation-qt/qthost.cpp | 61 ++- src/duckstation-qt/qthost.h | 6 + src/frontend-common/input_manager.cpp | 7 + 14 files changed, 1670 insertions(+), 15 deletions(-) create mode 100644 src/core/netplay.cpp create mode 100644 src/core/netplay.h create mode 100644 src/duckstation-qt/netplaywidget.cpp create mode 100644 src/duckstation-qt/netplaywidget.h create mode 100644 src/duckstation-qt/netplaywidget.ui diff --git a/src/core/netplay.cpp b/src/core/netplay.cpp new file mode 100644 index 000000000..b44b6962f --- /dev/null +++ b/src/core/netplay.cpp @@ -0,0 +1,265 @@ +#include "netplay.h" +#include "pad.h" +#include "spu.h" +#include "system.h" +#include + +// Netplay Impl +Netplay::Session::Session() = default; + +Netplay::Session::~Session() +{ + Close(); +} + +int32_t Netplay::Session::Start(int32_t lhandle, uint16_t lport, std::string& raddr, uint16_t rport, int32_t ldelay, + uint32_t pred) +{ + s_net_session.m_max_pred = pred; + /* + 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. + */ + GGPOSessionCallbacks cb{}; + + cb.advance_frame = NpAdvFrameCb; + cb.save_game_state = NpSaveFrameCb; + cb.load_game_state = NpLoadFrameCb; + cb.begin_game = NpBeginGameCb; + cb.free_buffer = NpFreeBuffCb; + cb.on_event = NpOnEventCb; + + GGPOErrorCode result; + + result = ggpo_start_session(&s_net_session.p_ggpo, &cb, "Duckstation-Netplay", 2, sizeof(Netplay::Input), lport, + s_net_session.m_max_pred); + + ggpo_set_disconnect_timeout(s_net_session.p_ggpo, 3000); + ggpo_set_disconnect_notify_start(s_net_session.p_ggpo, 1000); + + for (int i = 1; i <= 2; i++) + { + GGPOPlayer player = {}; + GGPOPlayerHandle handle = 0; + + player.size = sizeof(GGPOPlayer); + player.player_num = i; + + if (lhandle == i) + { + player.type = GGPOPlayerType::GGPO_PLAYERTYPE_LOCAL; + result = ggpo_add_player(s_net_session.p_ggpo, &player, &handle); + s_net_session.m_local_handle = handle; + } + else + { + player.type = GGPOPlayerType::GGPO_PLAYERTYPE_REMOTE; +#ifdef _WIN32 + strcpy_s(player.u.remote.ip_address, raddr.c_str()); +#else + strcpy(player.u.remote.ip_address, raddr.c_str()); +#endif + player.u.remote.port = rport; + result = ggpo_add_player(s_net_session.p_ggpo, &player, &handle); + } + } + ggpo_set_frame_delay(s_net_session.p_ggpo, s_net_session.m_local_handle, ldelay); + + return result; +} + +void Netplay::Session::Close() +{ + ggpo_close_session(s_net_session.p_ggpo); + s_net_session.p_ggpo = nullptr; + s_net_session.m_local_handle = GGPO_INVALID_HANDLE; + s_net_session.m_max_pred = 0; +} + +bool Netplay::Session::IsActive() +{ + return s_net_session.p_ggpo != nullptr; +} + +void Netplay::Session::RunIdle() +{ + ggpo_idle(s_net_session.p_ggpo); +} + +void Netplay::Session::AdvanceFrame(uint16_t checksum) +{ + ggpo_advance_frame(s_net_session.p_ggpo, checksum); +} + +void Netplay::Session::RunFrame(int32_t& waitTime) +{ + // run game + auto result = GGPO_OK; + int disconnectFlags = 0; + Netplay::Input inputs[2] = {}; + // add local input + if (GetLocalHandle() != GGPO_INVALID_HANDLE) + { + auto inp = ReadLocalInput(); + result = AddLocalInput(inp); + } + // advance game + if (GGPO_SUCCEEDED(result)) + { + result = SyncInput(inputs, &disconnectFlags); + if (GGPO_SUCCEEDED(result)) + { + // enable again when rolling back done + SPU::SetAudioOutputMuted(false); + System::NetplayAdvanceFrame (inputs, disconnectFlags); + } + else + RunIdle(); + } + else + RunIdle(); + + waitTime = GetTimer()->UsToWaitThisLoop(); +} + +int32_t Netplay::Session::CurrentFrame() +{ + int32_t frame; + ggpo_get_current_frame(s_net_session.p_ggpo, frame); + return frame; +} + +void Netplay::Session::CollectInput(uint32_t slot, uint32_t bind, float value) +{ + s_net_session.m_net_input[slot][bind] = value; +} + +Netplay::Input Netplay::Session::ReadLocalInput() +{ + // get controller data of the first controller (0 internally) + Netplay::Input inp{0}; + for (uint32_t i = 0; i < (uint32_t)DigitalController::Button::Count; i++) + { + if (s_net_session.m_net_input[0][i] >= 0.25f) + inp.button_data |= 1 << i; + } + return inp; +} + +std::string& Netplay::Session::GetGamePath() +{ + return s_net_session.m_game_path; +} + +void Netplay::Session::SetGamePath(std::string& path) +{ + s_net_session.m_game_path = path; +} + +void Netplay::Session::SendMsg(const char* msg) +{ + ggpo_client_chat(s_net_session.p_ggpo, msg); +} + +GGPOErrorCode Netplay::Session::SyncInput(Netplay::Input inputs[2], int* disconnect_flags) +{ + return ggpo_synchronize_input(s_net_session.p_ggpo, inputs, sizeof(Netplay::Input) * 2, disconnect_flags); +} + +GGPOErrorCode Netplay::Session::AddLocalInput(Netplay::Input input) +{ + return ggpo_add_local_input(s_net_session.p_ggpo, s_net_session.m_local_handle, &input, sizeof(Netplay::Input)); +} + +GGPONetworkStats& Netplay::Session::GetNetStats(int32_t handle) +{ + ggpo_get_network_stats(s_net_session.p_ggpo, handle, &s_net_session.m_last_net_stats); + return s_net_session.m_last_net_stats; +} + +int32_t Netplay::Session::GetPing() +{ + const int handle = GetLocalHandle() == 1 ? 2 : 1; + ggpo_get_network_stats(s_net_session.p_ggpo, handle, &s_net_session.m_last_net_stats); + return s_net_session.m_last_net_stats.network.ping; +} + +uint32_t Netplay::Session::GetMaxPrediction() +{ + return s_net_session.m_max_pred; +} + +GGPOPlayerHandle Netplay::Session::GetLocalHandle() +{ + return s_net_session.m_local_handle; +} + +void Netplay::Session::SetInputs(Netplay::Input inputs[2]) +{ + for (u32 i = 0; i < 2; i++) + { + auto cont = Pad::GetController(i); + std::bitset buttonBits(inputs[i].button_data); + for (u32 j = 0; j < (u32)DigitalController::Button::Count; j++) + cont->SetBindState(j, buttonBits.test(j) ? 1.0f : 0.0f); + } +} + +Netplay::LoopTimer* Netplay::Session::GetTimer() +{ + return &s_net_session.m_timer; +} + +uint16_t Netplay::Session::Fletcher16(uint8_t* data, int count) +{ + uint16_t sum1 = 0; + uint16_t sum2 = 0; + int index; + + for (index = 0; index < count; ++index) + { + sum1 = (sum1 + data[index]) % 255; + sum2 = (sum2 + sum1) % 255; + } + + return (sum2 << 8) | sum1; +} + +void Netplay::LoopTimer::Init(uint32_t fps, uint32_t frames_to_spread_wait) +{ + m_us_per_game_loop = 1000000 / fps; + m_us_ahead = 0; + m_us_extra_to_wait = 0; + m_frames_to_spread_wait = frames_to_spread_wait; + m_last_advantage = 0.0f; +} + +void Netplay::LoopTimer::OnGGPOTimeSyncEvent(float frames_ahead) +{ + m_last_advantage = (1000.0f * frames_ahead / 60.0f); + m_last_advantage /= 2; + if (m_last_advantage < 0) + { + int t = 0; + t++; + } + m_us_extra_to_wait = (int)(m_last_advantage * 1000); + if (m_us_extra_to_wait) + { + m_us_extra_to_wait /= m_frames_to_spread_wait; + m_wait_count = m_frames_to_spread_wait; + } +} + +int32_t Netplay::LoopTimer::UsToWaitThisLoop() +{ + int32_t timetoWait = m_us_per_game_loop; + if (m_wait_count) + { + timetoWait += m_us_extra_to_wait; + m_wait_count--; + if (!m_wait_count) + m_us_extra_to_wait = 0; + } + return timetoWait; +} diff --git a/src/core/netplay.h b/src/core/netplay.h new file mode 100644 index 000000000..e2659355c --- /dev/null +++ b/src/core/netplay.h @@ -0,0 +1,102 @@ +#pragma once + +#ifndef _NETPLAY_H +#define _NETPLAY_H + +#include +#include +#include +#include +#include + +#include "common/timer.h" +#include "digital_controller.h" +#include "types.h" + +// C GGPO Event Callbacks. Should be defined in system.cpp +extern "C" { +bool NpAdvFrameCb(void* ctx, int flags); +bool NpSaveFrameCb(void* ctx, uint8_t** buffer, int* len, int* checksum, int frame); +bool NpLoadFrameCb(void* ctx, uint8_t* buffer, int len, int rb_frames, int frame_to_load); +bool NpBeginGameCb(void* ctx, const char* game_name); +void NpFreeBuffCb(void* ctx, void* buffer); +bool NpOnEventCb(void* ctx, GGPOEvent* ev); +} + +namespace Netplay { + +struct Input +{ + uint32_t button_data; +}; + +struct LoopTimer +{ +public: + void Init(uint32_t fps, uint32_t frames_to_spread_wait); + void OnGGPOTimeSyncEvent(float frames_ahead); + // Call every loop, to get the amount of time the current iteration of gameloop should take + int32_t UsToWaitThisLoop(); + +private: + float m_last_advantage = 0.0f; + int32_t m_us_per_game_loop = 0; + int32_t m_us_ahead = 0; + int32_t m_us_extra_to_wait = 0; + int32_t m_frames_to_spread_wait = 0; + int32_t m_wait_count = 0; +}; + +class Session +{ +public: + Session(); + ~Session(); + // l = local, r = remote + static int32_t Start(int32_t lhandle, uint16_t lport, std::string& raddr, uint16_t rport, int32_t ldelay, + uint32_t pred); + + static void Close(); + static bool IsActive(); + static void RunIdle(); + + static void AdvanceFrame(uint16_t checksum = 0); + static void RunFrame(int32_t& waitTime); + static int32_t CurrentFrame(); + + static void CollectInput(uint32_t slot, uint32_t bind, float value); + static Netplay::Input ReadLocalInput(); + + static std::string& GetGamePath(); + static void SetGamePath(std::string& path); + static void SendMsg(const char* msg); + + static GGPOErrorCode SyncInput(Netplay::Input inputs[2], int* disconnect_flags); + static GGPOErrorCode AddLocalInput(Netplay::Input input); + static GGPONetworkStats& GetNetStats(int32_t handle); + static int32_t GetPing(); + static uint32_t GetMaxPrediction(); + static GGPOPlayerHandle GetLocalHandle(); + static void SetInputs(Netplay::Input inputs[2]); + + static Netplay::LoopTimer* GetTimer(); + static uint16_t Fletcher16(uint8_t* data, int count); + +private: + Netplay::LoopTimer m_timer; + std::string m_game_path; + uint32_t m_max_pred = 0; + + GGPOPlayerHandle m_local_handle = GGPO_INVALID_HANDLE; + GGPONetworkStats m_last_net_stats{}; + GGPOSession* p_ggpo = nullptr; + + std::array, NUM_CONTROLLER_AND_CARD_PORTS> m_net_input; +}; + +} // namespace Netplay + +// Netplay Instance +static Netplay::Session s_net_session = Netplay::Session(); + +#endif // !_NETPLAY_H diff --git a/src/core/pad.cpp b/src/core/pad.cpp index 43079a29c..f7cfdee01 100644 --- a/src/core/pad.cpp +++ b/src/core/pad.cpp @@ -169,12 +169,12 @@ void Pad::Reset() bool Pad::ShouldAvoidSavingToState() { // Currently only runahead, will also be used for netplay. - return g_settings.IsRunaheadEnabled(); + return g_settings.IsRunaheadEnabled() || Netplay::Session::IsActive(); } u32 Pad::GetMaximumRollbackFrames() { - return g_settings.runahead_frames; + return (Netplay::Session::IsActive() ? Netplay::Session::GetMaxPrediction() : g_settings.runahead_frames); } bool Pad::DoStateController(StateWrapper& sw, u32 i) diff --git a/src/core/system.cpp b/src/core/system.cpp index 2f7578d15..71738f712 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -33,6 +33,7 @@ #include "mdec.h" #include "memory_card.h" #include "multitap.h" +#include "netplay.h" #include "pad.h" #include "pgxp.h" #include "psf_loader.h" @@ -207,6 +208,8 @@ static std::deque s_runahead_states; static bool s_runahead_replay_pending = false; static u32 s_runahead_frames = 0; +static std::deque s_netplay_states; + static TinyString GetTimestampStringForFileName() { return TinyString::FromFmt("{:%Y-%m-%d_%H-%M-%S}", fmt::localtime(std::time(nullptr))); @@ -1538,7 +1541,7 @@ void System::Execute() // this can shut us down Host::PumpMessagesOnCPUThread(); - if (!IsValid()) + if (!IsValid() || Netplay::Session::IsActive()) return; if (s_frame_step_request) @@ -1565,6 +1568,38 @@ void System::Execute() } } +void System::ExecuteNetplay() +{ + // frame timing + s32 timeToWait; + std::chrono::steady_clock::time_point start, next, now; + start = next = now = std::chrono::steady_clock::now(); + while (Netplay::Session::IsActive() && System::IsRunning()) + { + now = std::chrono::steady_clock::now(); + if (now >= next) + { + Netplay::Session::RunFrame(timeToWait); + next = now + std::chrono::microseconds(timeToWait); + s_next_frame_time += timeToWait; + // this can shut us down + Host::PumpMessagesOnCPUThread(); + if (!IsValid() || !Netplay::Session::IsActive()) + break; + + const bool skip_present = g_host_display->ShouldSkipDisplayingFrame(); + Host::RenderDisplay(skip_present); + if (!skip_present && g_host_display->IsGPUTimingEnabled()) + { + s_accumulated_gpu_time += g_host_display->GetAndResetAccumulatedGPUTime(); + s_presents_since_last_update++; + } + + System::UpdatePerformanceCounters(); + } + } +} + void System::RecreateSystem() { Assert(!IsShutdown()); @@ -4431,3 +4466,163 @@ void System::SetTimerResolutionIncreased(bool enabled) timeEndPeriod(1); #endif } + +void System::StartNetplaySession(s32 local_handle, u16 local_port, std::string& remote_addr, u16 remote_port, + s32 input_delay, std::string& game_path) +{ + // dont want to start a session when theres already one going on. + if (Netplay::Session::IsActive()) + return; + // set game path for later loading during the begin game callback + Netplay::Session::SetGamePath(game_path); + // set netplay timer + const u32 fps = (s_region == ConsoleRegion::PAL ? 50 : 60); + Netplay::Session::GetTimer()->Init(fps, 180); + // create session + int result = Netplay::Session::Start(local_handle, local_port, remote_addr, remote_port, input_delay, 8); + if (result != GGPO_OK) + { + Log_ErrorPrintf("Failed to Create Netplay Session! Error: %d", result); + } +} + +void System::StopNetplaySession() +{ + if (!Netplay::Session::IsActive()) + return; + s_netplay_states.clear(); + Netplay::Session::Close(); +} + +void System::NetplayAdvanceFrame(Netplay::Input inputs[], int disconnect_flags) +{ + Netplay::Session::SetInputs(inputs); + System::DoRunFrame(); + Netplay::Session::AdvanceFrame(); +} + +bool 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(Netplay::Session::GetGamePath()); + param.override_fast_boot = true; + if (!System::BootSystem(param)) + { + System::StopNetplaySession(); + return false; + } + // Fast Forward to Game Start + SPU::SetAudioOutputMuted(true); + while (s_internal_frame_number < 2) + System::DoRunFrame(); + SPU::SetAudioOutputMuted(false); + return true; +} + +bool NpAdvFrameCb(void* ctx, int flags) +{ + Netplay::Input inputs[2] = {}; + int disconnectFlags; + Netplay::Session::SyncInput(inputs, &disconnectFlags); + System::NetplayAdvanceFrame(inputs, disconnectFlags); + return true; +} + +bool NpSaveFrameCb(void* ctx, uint8_t** buffer, int* len, int* checksum, int frame) +{ + bool result = false; + // give ggpo something so it doesnt complain. + u8 dummyData = 43; + *len = sizeof(u8); + *buffer = (unsigned char*)malloc(*len); + if (!*buffer) + return false; + memcpy(*buffer, &dummyData, *len); + // store state for later. + int pred = Netplay::Session::GetMaxPrediction(); + if (frame < pred && s_netplay_states.size() < pred) + { + MemorySaveState save; + result = System::SaveMemoryState(&save); + s_netplay_states.push_back(std::move(save)); + } + else + { + // reuse streams + result = System::SaveMemoryState(&s_netplay_states[frame % pred]); + } + return result; +} + +bool NpLoadFrameCb(void* ctx, uint8_t* buffer, int len, int rb_frames, int frame_to_load) +{ + // Disable Audio For upcoming rollback + SPU::SetAudioOutputMuted(true); + return System::LoadMemoryState(s_netplay_states[frame_to_load % Netplay::Session::GetMaxPrediction()]); +} + +bool NpOnEventCb(void* ctx, GGPOEvent* ev) +{ + char buff[128]; + std::string msg; + switch (ev->code) + { + case GGPOEventCode::GGPO_EVENTCODE_CONNECTED_TO_PEER: + sprintf(buff, "Netplay Connected To Player: %d", ev->u.connected.player); + msg = buff; + break; + case GGPOEventCode::GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER: + sprintf(buff, "Netplay Synchronzing: %d/%d", ev->u.synchronizing.count, ev->u.synchronizing.total); + msg = buff; + break; + case GGPOEventCode::GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER: + sprintf(buff, "Netplay Synchronized With Player: %d", ev->u.synchronized.player); + msg = buff; + break; + case GGPOEventCode::GGPO_EVENTCODE_DISCONNECTED_FROM_PEER: + sprintf(buff, "Netplay Player: %d Disconnected", ev->u.disconnected.player); + msg = buff; + break; + case GGPOEventCode::GGPO_EVENTCODE_RUNNING: + msg = "Netplay Is Running"; + break; + case GGPOEventCode::GGPO_EVENTCODE_CONNECTION_INTERRUPTED: + sprintf(buff, "Netplay Player: %d Connection Interupted, Timeout: %d", ev->u.connection_interrupted.player, + ev->u.connection_interrupted.disconnect_timeout); + msg = buff; + break; + case GGPOEventCode::GGPO_EVENTCODE_CONNECTION_RESUMED: + sprintf(buff, "Netplay Player: %d Connection Resumed", ev->u.connection_resumed.player); + msg = buff; + break; + case GGPOEventCode::GGPO_EVENTCODE_CHAT: + sprintf(buff, "%s", ev->u.chat.msg); + msg = buff; + break; + case GGPOEventCode::GGPO_EVENTCODE_TIMESYNC: + Netplay::Session::GetTimer()->OnGGPOTimeSyncEvent(ev->u.timesync.frames_ahead); + break; + case GGPOEventCode::GGPO_EVENTCODE_DESYNC: + sprintf(buff, "Netplay Desync Detected!: Frame: %d, L:%u, R:%u", ev->u.desync.nFrameOfDesync, + ev->u.desync.ourCheckSum, ev->u.desync.remoteChecksum); + msg = buff; + break; + default: + sprintf(buff, "Netplay Event Code: %d", ev->code); + msg = buff; + } + if (!msg.empty()) + { + Host::OnNetplayMessage(msg); + Log_InfoPrintf("%s", msg.c_str()); + } + return true; +} + +void NpFreeBuffCb(void* ctx, void* buffer) +{ + free(buffer); +} \ No newline at end of file diff --git a/src/core/system.h b/src/core/system.h index 7dfd22902..10c05274d 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -3,6 +3,7 @@ #pragma once #include "common/timer.h" +#include "netplay.h" #include "settings.h" #include "timing_event.h" #include "types.h" @@ -19,11 +20,10 @@ class Controller; struct CheatCode; class CheatList; -namespace BIOS -{ +namespace BIOS { struct ImageInfo; struct Hash; -} +} // namespace BIOS struct SystemBootParameters { @@ -227,6 +227,9 @@ bool SaveResumeState(); /// Runs the VM until the CPU execution is canceled. void Execute(); +/// Runs the VM and netplay loop. when the netplay loop cancels it switches to normal execute mode. +void ExecuteNetplay(); + /// Switches the GPU renderer by saving state, recreating the display window, and restoring state (if needed). void RecreateSystem(); @@ -452,6 +455,11 @@ void UpdateMemorySaveStateSettings(); bool LoadRewindState(u32 skip_saves = 0, bool consume_state = true); void SetRunaheadReplayFlag(); +/// Netplay +void StartNetplaySession(s32 local_handle, u16 local_port, std::string& remote_addr, u16 remote_port, s32 input_delay, + std::string& game_path); +void StopNetplaySession(); +void NetplayAdvanceFrame(Netplay::Input inputs[], int disconnect_flags); } // namespace System namespace Host { @@ -500,4 +508,6 @@ bool IsFullscreen(); /// Alters fullscreen state of hosting application. void SetFullscreen(bool enabled); +// netplay +void OnNetplayMessage(std::string& message); } // namespace Host diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index b7174b3fc..5d7d61cf4 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -50,6 +50,7 @@ #ifdef _WIN32 #include "common/windows_headers.h" #include +#include #endif Log_SetChannel(MainWindow); @@ -105,6 +106,12 @@ MainWindow::MainWindow() : QMainWindow(nullptr) #if !defined(_WIN32) && !defined(__APPLE__) s_use_central_widget = DisplayContainer::isRunningOnWayland(); #endif + +#if defined(_WIN32) + // Setup WinSock + WSADATA wd = {}; + WSAStartup(MAKEWORD(2, 2), &wd); +#endif } MainWindow::~MainWindow() @@ -119,6 +126,8 @@ MainWindow::~MainWindow() #ifdef _WIN32 unregisterForDeviceNotifications(); + // Cleanup WinSock + WSACleanup(); #endif #ifdef __APPLE__ FrontendCommon::RemoveThemeChangeHandler(this); @@ -1556,9 +1565,13 @@ void MainWindow::setupAdditionalUi() m_status_fps_widget->hide(); m_status_vps_widget = new QLabel(m_ui.statusBar); - m_status_vps_widget->setFixedSize(125, 16); + m_status_vps_widget->setFixedSize(120, 16); m_status_vps_widget->hide(); + m_status_ping_widget = new QLabel(m_ui.statusBar); + m_status_ping_widget->setFixedSize(110, 16); + m_status_ping_widget->hide(); + m_settings_toolbar_menu = new QMenu(m_ui.toolBar); m_settings_toolbar_menu->addAction(m_ui.actionSettings); m_settings_toolbar_menu->addAction(m_ui.actionViewGameProperties); @@ -1764,6 +1777,7 @@ void MainWindow::updateStatusBarWidgetVisibility() Update(m_status_resolution_widget, s_system_valid && !s_system_paused, 0); Update(m_status_fps_widget, s_system_valid && !s_system_paused, 0); Update(m_status_vps_widget, s_system_valid && !s_system_paused, 0); + Update(m_status_ping_widget, s_system_valid && !s_system_paused && m_netplay_window != nullptr, 0); } void MainWindow::updateWindowTitle() @@ -2089,6 +2103,9 @@ void MainWindow::connectSignals() addThemeToMenu(tr("Dark Fusion (Blue)"), QStringLiteral("darkfusionblue")); addThemeToMenu(tr("QDarkStyle"), QStringLiteral("qdarkstyle")); updateMenuSelectedTheme(); + + // Netplay UI , TODO + connect(m_ui.actionCreateNetplaySession, &QAction::triggered, this, &MainWindow::onNetplaySessionCreated); } void MainWindow::addThemeToMenu(const QString& name, const QString& key) @@ -2752,6 +2769,28 @@ void MainWindow::onCPUDebuggerClosed() m_debugger_window = nullptr; } +void MainWindow::onNetplaySessionCreated() +{ + Assert(!m_netplay_window); + + m_netplay_window = new NetplayWidget(this); + m_netplay_window->setWindowIcon(windowIcon()); + m_netplay_window->setWindowTitle("Netplay Session"); + m_netplay_window->setWindowFlags(windowFlags() | Qt::WindowMinimizeButtonHint); + m_netplay_window->show(); + + m_ui.menuNetplay->setDisabled(true); + + connect(m_netplay_window, &NetplayWidget::finished, [this]() { + Assert(m_netplay_window); + + m_netplay_window->deleteLater(); + m_netplay_window = nullptr; + + m_ui.menuNetplay->setDisabled(false); + }); +} + void MainWindow::onToolsOpenDataDirectoryTriggered() { QtUtils::OpenURL(this, QUrl::fromLocalFile(QString::fromStdString(EmuFolders::DataRoot))); diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index 363adf490..6aa3ecc0c 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -16,6 +16,7 @@ #include "displaywidget.h" #include "settingsdialog.h" #include "ui_mainwindow.h" +#include class QLabel; class QThread; @@ -89,6 +90,7 @@ public: ALWAYS_INLINE QLabel* getStatusResolutionWidget() const { return m_status_resolution_widget; } ALWAYS_INLINE QLabel* getStatusFPSWidget() const { return m_status_fps_widget; } ALWAYS_INLINE QLabel* getStatusVPSWidget() const { return m_status_vps_widget; } + ALWAYS_INLINE QLabel* getStatusPingWidget() const { return m_status_ping_widget; } public Q_SLOTS: /// Updates debug menu visibility (hides if disabled). @@ -168,6 +170,8 @@ private Q_SLOTS: void openCPUDebugger(); void onCPUDebuggerClosed(); + void onNetplaySessionCreated(); + protected: void showEvent(QShowEvent* event) override; void closeEvent(QCloseEvent* event) override; @@ -262,6 +266,7 @@ private: QLabel* m_status_renderer_widget = nullptr; QLabel* m_status_fps_widget = nullptr; QLabel* m_status_vps_widget = nullptr; + QLabel* m_status_ping_widget = nullptr; QLabel* m_status_resolution_widget = nullptr; QMenu* m_settings_toolbar_menu = nullptr; @@ -273,6 +278,7 @@ private: MemoryCardEditorDialog* m_memory_card_editor_dialog = nullptr; CheatManagerDialog* m_cheat_manager_dialog = nullptr; DebuggerWindow* m_debugger_window = nullptr; + NetplayWidget* m_netplay_window = nullptr; std::string m_current_game_title; std::string m_current_game_serial; diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index 2fa457133..66ff93c02 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -17,7 +17,7 @@ DuckStation - + :/icons/duck.png:/icons/duck.png @@ -27,7 +27,7 @@ 0 0 800 - 22 + 21 @@ -237,8 +237,15 @@ + + + Netplay + + + + @@ -472,7 +479,7 @@ - + :/icons/github.png:/icons/github.png @@ -481,7 +488,7 @@ - + :/icons/IssueTracker.png:/icons/IssueTracker.png @@ -490,7 +497,7 @@ - + :/icons/discord.png:/icons/discord.png @@ -508,7 +515,7 @@ - + :/icons/QT.png:/icons/QT.png @@ -517,7 +524,7 @@ - + :/icons/duck_64.png:/icons/duck_64.png @@ -976,6 +983,11 @@ Cover Downloader + + + Create Session + + diff --git a/src/duckstation-qt/netplaywidget.cpp b/src/duckstation-qt/netplaywidget.cpp new file mode 100644 index 000000000..d598d89be --- /dev/null +++ b/src/duckstation-qt/netplaywidget.cpp @@ -0,0 +1,210 @@ +#include "netplaywidget.h" +#include "ui_netplaywidget.h" +#include +#include +#include +#include + +Log_SetChannel(NetplayWidget); + +NetplayWidget::NetplayWidget(QWidget* parent) : QDialog(parent), m_ui(new Ui::NetplayWidget) +{ + m_ui->setupUi(this); + FillGameList(); + SetupConnections(); + SetupConstraints(); + CheckControllersSet(); +} + +NetplayWidget::~NetplayWidget() +{ + StopSession(); + delete m_ui; +} + +void NetplayWidget::FillGameList() +{ + // Get all games and fill the list later to know which game to boot. + s32 numGames = GameList::GetEntryCount(); + for (s32 i = 0; i < numGames; i++) + { + const auto& entry = GameList::GetEntryByIndex(i); + std::string baseFilename = entry->path.substr(entry->path.find_last_of("/\\") + 1); + m_ui->cbSelectedGame->addItem( + QString::fromStdString("[" + entry->serial + "] " + entry->title + " | " + baseFilename)); + m_available_games.push_back(entry->path); + } +} + +void NetplayWidget::SetupConnections() +{ + // connect netplay window messages + connect(g_emu_thread, &EmuThread::onNetplayMessage, this, &NetplayWidget::OnMsgReceived); + // connect sending messages when the chat button has been pressed + connect(m_ui->btnSendMsg, &QPushButton::pressed, [this]() { + // check if message aint empty and the complete message ( message + name + ":" + space) is below 120 characters + auto msg = m_ui->tbNetplayChat->toPlainText().trimmed(); + QString completeMsg = m_ui->lePlayerName->text().trimmed() + ": " + msg; + if (completeMsg.length() > 120) + return; + m_ui->lwChatWindow->addItem(completeMsg); + m_ui->tbNetplayChat->clear(); + if (!g_emu_thread) + return; + g_emu_thread->sendNetplayMessage(completeMsg); + }); + + // switch between DIRECT IP and traversal options + connect(m_ui->cbConnMode, &QComboBox::currentIndexChanged, [this]() { + // zero is DIRECT IP mode + const bool action = (m_ui->cbConnMode->currentIndex() == 0 ? true : false); + m_ui->frDirectIP->setVisible(action); + m_ui->frDirectIP->setEnabled(action); + m_ui->btnStartSession->setEnabled(action); + m_ui->tabTraversal->setEnabled(!action); + m_ui->btnTraversalJoin->setEnabled(!action); + m_ui->btnTraversalHost->setEnabled(!action); + }); + + // actions to be taken when stopping a session. + auto fnOnStopSession = [this]() { + m_ui->btnSendMsg->setEnabled(false); + m_ui->tbNetplayChat->setEnabled(false); + m_ui->btnStopSession->setEnabled(false); + m_ui->btnStartSession->setEnabled(true); + m_ui->btnTraversalHost->setEnabled(true); + m_ui->btnTraversalJoin->setEnabled(true); + m_ui->lblHostCodeResult->setText("XXXXXXXXX-"); + StopSession(); + }; + + // check session when start button pressed if there is the needed info depending on the connection mode + auto fnCheckValid = [this, fnOnStopSession]() { + const bool action = (m_ui->cbConnMode->currentIndex() == 0 ? true : false); + if (CheckInfoValid(action)) + { + m_ui->btnSendMsg->setEnabled(true); + m_ui->tbNetplayChat->setEnabled(true); + m_ui->btnStopSession->setEnabled(true); + m_ui->btnStartSession->setEnabled(false); + m_ui->btnTraversalHost->setEnabled(false); + m_ui->btnTraversalJoin->setEnabled(false); + if (!StartSession(action)) + fnOnStopSession(); + } + }; + connect(m_ui->btnStartSession, &QPushButton::pressed, fnCheckValid); + connect(m_ui->btnTraversalJoin, &QPushButton::pressed, fnCheckValid); + connect(m_ui->btnTraversalHost, &QPushButton::pressed, fnCheckValid); + // when pressed revert back to the previous ui state so people can start a new session. + connect(m_ui->btnStopSession, &QPushButton::pressed, fnOnStopSession); +} + +void NetplayWidget::SetupConstraints() +{ + m_ui->lwChatWindow->setWordWrap(true); + m_ui->sbLocalPort->setRange(0, 65535); + m_ui->sbRemotePort->setRange(0, 65535); + m_ui->sbInputDelay->setRange(0, 10); + m_ui->leRemoteAddr->setMaxLength(15); + m_ui->lePlayerName->setMaxLength(12); + QString IpRange = "(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])"; + QRegularExpression IpRegex("^" + IpRange + "(\\." + IpRange + ")" + "(\\." + IpRange + ")" + "(\\." + IpRange + ")$"); + QRegularExpressionValidator* ipValidator = new QRegularExpressionValidator(IpRegex, this); + m_ui->leRemoteAddr->setValidator(ipValidator); +} + +bool NetplayWidget::CheckInfoValid(bool direct_ip) +{ + if (!direct_ip) + { + QMessageBox errBox; + errBox.setFixedSize(500, 200); + errBox.information(this, "Netplay Session", "Traversal Mode is not supported yet!"); + errBox.show(); + return false; + } + + bool err = false; + // check nickname, game selected and player selected. + if (m_ui->lePlayerName->text().trimmed().isEmpty() || m_ui->cbSelectedGame->currentIndex() == 0 || + m_ui->cbLocalPlayer->currentIndex() == 0) + err = true; + // check if direct ip details have been filled in + if (direct_ip && (m_ui->leRemoteAddr->text().trimmed().isEmpty() || m_ui->sbRemotePort->value() == 0 || + m_ui->sbLocalPort->value() == 0)) + err = true; + // check if host code has been filled in + if (!direct_ip && m_ui->leHostCode->text().trimmed().isEmpty() && + m_ui->tabTraversal->currentWidget() == m_ui->tabJoin) + err = true; + // if an err has been found throw + if (err) + { + QMessageBox errBox; + errBox.setFixedSize(500, 200); + errBox.information(this, "Netplay Session", "Please fill in all the needed fields!"); + errBox.show(); + return !err; + } + // check if controllers are set + err = !CheckControllersSet(); + // everything filled in. inverse cuz we would like to return true if the info is valid. + return !err; +} + +bool NetplayWidget::CheckControllersSet() +{ + bool err = false; + // check whether its controllers are set right + for (u32 i = 0; i < 2; i++) + { + const Controller::ControllerInfo* cinfo = Controller::GetControllerInfo(g_settings.controller_types[i]); + if (!cinfo || cinfo->type != ControllerType::DigitalController) + { + err = true; + } + } + // if an err has been found throw popup + if (err) + { + QMessageBox errBox; + errBox.information(this, "Netplay Session", + "Please make sure the controllers are both enabled and set as Digital Controllers"); + errBox.setFixedSize(500, 200); + errBox.show(); + } + // controllers are set right + return !err; +} + +bool NetplayWidget::StartSession(bool direct_ip) +{ + if (!g_emu_thread) + return false; + + int localHandle = m_ui->cbLocalPlayer->currentIndex(); + int inputDelay = m_ui->sbInputDelay->value(); + quint16 localPort = m_ui->sbLocalPort->value(); + const QString& remoteAddr = m_ui->leRemoteAddr->text(); + quint16 remotePort = m_ui->sbRemotePort->value(); + const QString& gamePath = QString::fromStdString(m_available_games[m_ui->cbSelectedGame->currentIndex() - 1]); + + if (!direct_ip) + return false; // TODO: Handle Nat Traversal and use that information by overriding the information above. + + g_emu_thread->startNetplaySession(localHandle, localPort, remoteAddr, remotePort, inputDelay, gamePath); + return true; +} + +void NetplayWidget::StopSession() +{ + if (!g_emu_thread) + return; + g_emu_thread->stopNetplaySession(); +} + +void NetplayWidget::OnMsgReceived(const QString& msg) +{ + m_ui->lwChatWindow->addItem(msg); +} diff --git a/src/duckstation-qt/netplaywidget.h b/src/duckstation-qt/netplaywidget.h new file mode 100644 index 000000000..fcbc80ad3 --- /dev/null +++ b/src/duckstation-qt/netplaywidget.h @@ -0,0 +1,34 @@ +#ifndef NETPLAYWIDGET_H +#define NETPLAYWIDGET_H + +#include +#include + +namespace Ui { +class NetplayWidget; +} + +class NetplayWidget : public QDialog +{ + Q_OBJECT + +public: + explicit NetplayWidget(QWidget* parent = nullptr); + ~NetplayWidget(); + +private: + void FillGameList(); + void SetupConnections(); + void SetupConstraints(); + bool CheckInfoValid(bool direct_ip); + bool CheckControllersSet(); + bool StartSession(bool direct_ip); + void StopSession(); + void OnMsgReceived(const QString& msg); + +private: + Ui::NetplayWidget* m_ui; + std::vector m_available_games; +}; + +#endif // NETPLAYWIDGET_H diff --git a/src/duckstation-qt/netplaywidget.ui b/src/duckstation-qt/netplaywidget.ui new file mode 100644 index 000000000..8effbdb5c --- /dev/null +++ b/src/duckstation-qt/netplaywidget.ui @@ -0,0 +1,710 @@ + + + NetplayWidget + + + + 0 + 0 + 700 + 448 + + + + + 0 + 0 + + + + + 700 + 448 + + + + + 700 + 448 + + + + + true + + + + Dialog + + + false + + + + + 0 + 0 + 701 + 461 + + + + + 0 + 0 + + + + + 701 + 461 + + + + + 701 + 461 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + 0 + 40 + 711 + 20 + + + + Qt::Horizontal + + + + + false + + + + 380 + 400 + 231 + 41 + + + + + 10 + true + + + + ... + + + + + + false + + + + 620 + 400 + 61 + 41 + + + + + 11 + true + + + + Send + + + + + + 350 + 50 + 21 + 411 + + + + + 9 + false + false + false + true + + + + Qt::Vertical + + + + + + 380 + 60 + 301 + 331 + + + + + 11 + true + + + + Qt::ScrollBarAsNeeded + + + Qt::ScrollBarAlwaysOff + + + + + + + + + + true + + + + 9 + 220 + 341 + 171 + + + + true + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + 10 + 0 + 321 + 161 + + + + + + + + 11 + true + + + + 65535 + + + 1 + + + QAbstractSpinBox::DefaultStepType + + + 2000 + + + + + + + + 11 + true + + + + + + + + + + + + 11 + true + + + + Local Port : + + + + + + + + 11 + true + + + + Remote Address : + + + + + + + + 11 + true + + + + Remote Port : + + + + + + + true + + + + 11 + true + + + + Start Session + + + + + + + + 11 + true + + + + 65535 + + + 1 + + + QAbstractSpinBox::DefaultStepType + + + 1000 + + + + + + + + + + 10 + 10 + 61 + 31 + + + + + 12 + true + + + + Game : + + + + + + 70 + 10 + 611 + 31 + + + + + 10 + true + true + + + + 8 + + + + SELECT A GAME + + + + + + false + + + + 100 + 400 + 161 + 41 + + + + + 10 + true + + + + Stop Session + + + + + true + + + + 20 + 220 + 321 + 171 + + + + + 11 + true + + + + 0 + + + + Join + + + + + 220 + 60 + 81 + 31 + + + + + 11 + true + + + + Join + + + + + + 10 + 20 + 291 + 28 + + + + + + + true + + + + 11 + true + + + + Host Code : + + + + + + + + 11 + true + + + + + + + + + + + 10 + true + + + + Host + + + + + 10 + 20 + 81 + 31 + + + + + 11 + true + + + + Host + + + + + + 10 + 60 + 281 + 31 + + + + + + + + 11 + true + + + + Host Code : + + + + + + + + 11 + true + + + + XXXXXXXXX- + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + 20 + 60 + 321 + 151 + + + + + + + + 11 + true + + + + Player : + + + + + + + + 11 + true + true + + + + + NOT SET + + + + + 1 + + + + + 2 + + + + + + + + + 11 + true + + + + Input Delay : + + + + + + + + 11 + true + + + + Duck! + + + + + + + + 11 + true + + + + 30 + + + 1 + + + QAbstractSpinBox::DefaultStepType + + + 1 + + + + + + + + 11 + true + true + + + + + DIRECT IP + + + + + TRAVERSAL + + + + + + + + + 11 + true + + + + Connection Mode : + + + + + + + + 11 + true + + + + Nickname : + + + + + + tabTraversal + frDirectIP + line_2 + tbNetplayChat + btnSendMsg + line + lwChatWindow + lblGame + cbSelectedGame + btnStopSession + gridLayoutWidget + + + + + diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 3a7fd50ad..9dc241444 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -461,6 +461,12 @@ void EmuThread::startFullscreenUI() wakeThread(); } +void Host::OnNetplayMessage(std::string& message) +{ + QString msg(message.c_str()); + emit g_emu_thread->onNetplayMessage(msg); +} + void EmuThread::stopFullscreenUI() { if (!isOnThread()) @@ -1065,6 +1071,48 @@ void EmuThread::reloadPostProcessingShaders() System::ReloadPostProcessingShaders(); } +void EmuThread::startNetplaySession(int local_handle, quint16 local_port, const QString& remote_addr, + quint16 remote_port, int input_delay, const QString& game_path) +{ + if (!isOnThread()) + { + QMetaObject::invokeMethod(this, "startNetplaySession", Qt::QueuedConnection, Q_ARG(int, local_handle), + Q_ARG(quint16, local_port), Q_ARG(const QString&, remote_addr), + Q_ARG(quint16, remote_port), Q_ARG(int, input_delay), Q_ARG(const QString&, game_path)); + return; + } + // disable block linking and disable rewind and runahead during a netplay session + g_settings.cpu_recompiler_block_linking = false; + g_settings.rewind_enable = false; + g_settings.runahead_frames = 0; + + Log_WarningPrintf("Disabling block linking, runahead and rewind due to rollback."); + + auto remAddr = remote_addr.trimmed().toStdString(); + auto gamePath = game_path.trimmed().toStdString(); + System::StartNetplaySession(local_handle, local_port, remAddr, remote_port, input_delay, gamePath); +} + +void EmuThread::sendNetplayMessage(const QString& message) +{ + if (!isOnThread()) + { + QMetaObject::invokeMethod(this, "sendNetplayMessage", Qt::QueuedConnection, Q_ARG(const QString&, message)); + return; + } + Netplay::Session::SendMsg(message.toStdString().c_str()); +} + +void EmuThread::stopNetplaySession() +{ + if (!isOnThread()) + { + QMetaObject::invokeMethod(this, "stopNetplaySession", Qt::QueuedConnection); + return; + } + System::StopNetplaySession(); +} + void EmuThread::runOnEmuThread(std::function callback) { callback(); @@ -1416,7 +1464,10 @@ void EmuThread::run() { if (System::IsRunning()) { - System::Execute(); + if (Netplay::Session::IsActive()) + System::ExecuteNetplay(); + else + System::Execute(); } else { @@ -1659,6 +1710,14 @@ void EmuThread::updatePerformanceCounters() m_last_speed = speed; m_last_video_fps = vfps; } + + const s32 ping = Netplay::Session::GetPing(); + if (m_last_ping != ping) + { + QMetaObject::invokeMethod(g_main_window->getStatusPingWidget(), "setText", Qt::QueuedConnection, + Q_ARG(const QString&, tr("Netplay Ping: %1 ").arg(ping, 0, 'f', 0))); + m_last_ping = ping; + } } void EmuThread::resetPerformanceCounters() diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index d45be2460..9ca0e1ba7 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -143,6 +143,7 @@ Q_SIGNALS: void achievementsRefreshed(quint32 id, const QString& game_info_string, quint32 total, quint32 points); void achievementsChallengeModeChanged(); void cheatEnabled(quint32 index, bool enabled); + void onNetplayMessage(const QString& message); public Q_SLOTS: void setDefaultSettings(bool system = true, bool controller = true); @@ -187,6 +188,10 @@ public Q_SLOTS: void setCheatEnabled(quint32 index, bool enabled); void applyCheat(quint32 index); void reloadPostProcessingShaders(); + void startNetplaySession(int local_handle, quint16 local_port, const QString& remote_addr, quint16 remote_port, + int input_delay, const QString& game_path); + void stopNetplaySession(); + void sendNetplayMessage(const QString& message); private Q_SLOTS: void stopInThread(); @@ -232,6 +237,7 @@ private: float m_last_video_fps = std::numeric_limits::infinity(); u32 m_last_render_width = std::numeric_limits::max(); u32 m_last_render_height = std::numeric_limits::max(); + u32 m_last_ping = std::numeric_limits::max(); GPURenderer m_last_renderer = GPURenderer::Count; }; diff --git a/src/frontend-common/input_manager.cpp b/src/frontend-common/input_manager.cpp index a22d9eb22..aba111cf4 100644 --- a/src/frontend-common/input_manager.cpp +++ b/src/frontend-common/input_manager.cpp @@ -11,6 +11,7 @@ #include "core/controller.h" #include "core/host.h" #include "core/system.h" +#include "core/netplay.h" #include "imgui_manager.h" #include "input_source.h" @@ -713,6 +714,12 @@ void InputManager::AddPadBindings(SettingsInterface& si, const std::string& sect if (!System::IsValid()) return; + if (Netplay::Session::IsActive()) + { + Netplay::Session::CollectInput(pad_index, bind_index, value); + return; + } + Controller* c = System::GetController(pad_index); if (c) c->SetBindState(bind_index, value);