From 7ba2d5c94e743a5bbd3627b35a7da4145ad3b095 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Tue, 16 May 2023 21:58:06 +1000 Subject: [PATCH] Netplay: Match peer settings to host --- src/core/netplay.cpp | 164 ++++++++++++++++++++++++++++------ src/core/netplay_packets.h | 42 ++++++++- src/core/system.cpp | 63 ++----------- src/core/system.h | 2 +- src/duckstation-qt/qthost.cpp | 10 ++- 5 files changed, 189 insertions(+), 92 deletions(-) diff --git a/src/core/netplay.cpp b/src/core/netplay.cpp index 9013cdaa8..b5953ecad 100644 --- a/src/core/netplay.cpp +++ b/src/core/netplay.cpp @@ -10,6 +10,7 @@ #include "common/timer.h" #include "digital_controller.h" #include "host.h" +#include "host_display.h" #include "host_settings.h" #include "netplay_packets.h" #include "pad.h" @@ -62,7 +63,8 @@ static GGPOErrorCode AddLocalInput(Netplay::Input input); static GGPOErrorCode SyncInput(Input inputs[2], int* disconnect_flags); static void SetInputs(Input inputs[2]); -static void SetSettings(); +static void SetSettings(const ConnectResponseMessage* msg); +static void FillSettings(ConnectResponseMessage* msg); static bool CreateDummySystem(); static void CloseSessionWithError(const std::string_view& message); @@ -198,6 +200,8 @@ struct PacketWrapper ALWAYS_INLINE const T* operator->() const { return reinterpret_cast(pkt->data); } ALWAYS_INLINE T* operator->() { return reinterpret_cast(pkt->data); } + ALWAYS_INLINE const T* operator&() const { return reinterpret_cast(pkt->data); } + ALWAYS_INLINE T* operator&() { return reinterpret_cast(pkt->data); } }; template static PacketWrapper NewWrappedPacket(u32 size = sizeof(T), u32 flags = 0) @@ -300,24 +304,30 @@ bool Netplay::Start(bool is_hosting, std::string nickname, const std::string& re } // Need a system if we're hosting. - if (!System::IsValid()) + if (is_hosting) { - if (is_hosting) + if (!System::IsValid()) { Log_ErrorPrintf("Can't host a netplay session without a valid VM"); return false; } - else if (!is_hosting && !CreateDummySystem()) + } + else + { + // We shouldn't have a system, toss it if we do. + if (System::IsValid()) + System::ShutdownSystem(false); + + // But we need the display to show the connecting screen. + if (!Host::AcquireHostDisplay(Settings::GetRenderAPIForRenderer(g_settings.gpu_renderer))) { - Log_ErrorPrintf("Failed to create VM for joining session"); + Log_ErrorPrintf("Failed to get host display for netplay"); return false; } } s_state = SessionState::Initializing; - SetSettings(); - if (!InitializeEnet()) { Log_ErrorPrintf("Failed to initialize Enet."); @@ -348,9 +358,11 @@ bool Netplay::Start(bool is_hosting, std::string nickname, const std::string& re s_player_id = 0; s_num_players = 1; s_reset_players = 1; + s_peers[s_player_id].nickname = s_local_nickname; CreateGGPOSession(); s_state = SessionState::Running; Log_InfoPrintf("Netplay session started as host on port %d.", port); + SetSettings(nullptr); System::SetState(System::State::Paused); return true; } @@ -378,7 +390,6 @@ bool Netplay::Start(bool is_hosting, std::string nickname, const std::string& re s_state = SessionState::Connecting; s_reset_start_time.Reset(); s_last_host_connection_attempt.Reset(); - System::SetState(System::State::Paused); return true; } @@ -496,6 +507,9 @@ void Netplay::RequestCloseSession(CloseSessionMessage::Reason reason) Host::DisplayLoadingScreen("Closing session"); Host::PumpMessagesOnCPUThread(); } + + // toss host display, if we were still connecting, this'll be up + Host::ReleaseHostDisplay(); } bool Netplay::InitializeEnet() @@ -839,12 +853,12 @@ void Netplay::HandlePeerConnectionAsHost(ENetPeer* peer) sizeof(ConnectResponseMessage) + static_cast(game_serial.length()) + static_cast(game_title.length())); pkt->num_players = s_num_players; pkt->max_players = MAX_PLAYERS; - pkt->console_region = System::GetRegion(); pkt->game_title_length = static_cast(game_title.length()); pkt->game_serial_length = static_cast(game_serial.length()); pkt->game_hash = System::GetGameHash(); pkt->bios_hash = System::GetBIOSHash(); - pkt->was_fast_booted = System::WasFastBooted(); + FillSettings(&pkt); + std::memcpy(pkt.pkt->data + sizeof(ConnectResponseMessage), game_serial.c_str(), pkt->game_serial_length); std::memcpy(pkt.pkt->data + sizeof(ConnectResponseMessage) + pkt->game_serial_length, game_title.c_str(), pkt->game_title_length); @@ -874,14 +888,14 @@ void Netplay::HandleConnectResponseMessage(s32 player_id, const ENetPacket* pkt) } Log_InfoPrintf("Received session details from host: "); - Log_InfoPrintf(" Console Region: %s", Settings::GetConsoleRegionDisplayName(msg->console_region)); - Log_InfoPrintf(" BIOS Hash: %s%s", msg->bios_hash.ToString().c_str(), msg->was_fast_booted ? " (fast booted)" : ""); + Log_InfoPrintf(" Console Region: %s", Settings::GetConsoleRegionDisplayName(msg->settings.console_region)); + Log_InfoPrintf(" BIOS Hash: %s%s", msg->bios_hash.ToString().c_str(), msg->settings.was_fast_booted ? " (fast booted)" : ""); Log_InfoPrintf(" Game Serial: %.*s", msg->game_serial_length, msg->GetGameSerial().data()); Log_InfoPrintf(" Game Title: %.*s", msg->game_title_length, msg->GetGameTitle().data()); Log_InfoPrintf(" Game Hash: %" PRIX64, msg->game_hash); // Find a matching BIOS. - const std::string bios_path = BIOS::FindBIOSPathWithHash(EmuFolders::Bios.c_str(), msg->bios_hash); + std::string bios_path = BIOS::FindBIOSPathWithHash(EmuFolders::Bios.c_str(), msg->bios_hash); if (bios_path.empty()) { CloseSessionWithError(fmt::format( @@ -891,8 +905,14 @@ void Netplay::HandleConnectResponseMessage(s32 player_id, const ENetPacket* pkt) } // Find the matching game. - const GameList::Entry* entry = GameList::GetEntryBySerialAndHash(msg->GetGameSerial(), msg->game_hash); - if (!entry) + std::string game_path; + { + auto lock = GameList::GetLock(); + const GameList::Entry* entry = GameList::GetEntryBySerialAndHash(msg->GetGameSerial(), msg->game_hash); + if (entry) + game_path = entry->path; + } + if (game_path.empty()) { CloseSessionWithError(fmt::format( Host::TranslateString("Netplay", "Cannot join session: Unable to find game \"{}\".\nSerial: {}\nHash: {}") @@ -902,12 +922,19 @@ void Netplay::HandleConnectResponseMessage(s32 player_id, const ENetPacket* pkt) } Log_InfoPrintf("Found matching BIOS: %s", bios_path.c_str()); - Log_InfoPrintf("Found matching game: %s", entry->path.c_str()); + Log_InfoPrintf("Found matching game: %s", game_path.c_str()); - // Reboot created system with host details. - if (!System::ReinitializeSystem(msg->console_region, bios_path.c_str(), entry->path.c_str(), msg->was_fast_booted)) + // Apply settings from host. + SetSettings(msg); + + // Create system with host details. + Assert(!System::IsValid()); + SystemBootParameters params; + params.filename = std::move(game_path); + params.override_bios = std::move(bios_path); + if (!System::BootSystem(std::move(params))) { - CloseSessionWithError(Host::TranslateStdString("Netplay", "Cannot join session: Failed to reinitialize system.")); + CloseSessionWithError(Host::TranslateStdString("Netplay", "Cannot join session: Failed to boot system.")); return; } @@ -1512,7 +1539,7 @@ void Netplay::HandleChatMessage(s32 player_id, const ENetPacket* pkt) // Settings Overlay ////////////////////////////////////////////////////////////////////////// -void Netplay::SetSettings() +void Netplay::SetSettings(const ConnectResponseMessage* msg) { MemorySettingsInterface& si = s_settings_overlay; @@ -1537,26 +1564,106 @@ void Netplay::SetSettings() // si.SetStringValue("CPU", "ExecutionMode", "Interpreter"); - // BIOS patching must be the same. - si.SetBoolValue("BIOS", "PatchTTYEnable", false); - si.SetBoolValue("BIOS", "PatchFastBoot", true); - si.SetBoolValue("CDROM", "LoadImagePatches", false); - // No runahead or rewind, that'd be a disaster. si.SetIntValue("Main", "RunaheadFrameCount", 0); si.SetBoolValue("Main", "RewindEnable", false); // no block linking, it degrades savestate loading performance si.SetBoolValue("CPU", "RecompilerBlockLinking", false); - // not sure its needed but enabled for now... TODO + + // Turn off fastmem, it can affect determinism depending on when it was loaded. + si.SetBoolValue("CPU", "FastmemMode", Settings::GetCPUFastmemModeName(CPUFastmemMode::Disabled)); + + // SW renderer for readbacks ensures differences in host GPU don't affect downloads. si.SetBoolValue("GPU", "UseSoftwareRendererForReadbacks", true); - // TODO: PGXP should be the same as the host, as should overclock etc. + // No cheats.. yet. Need to serialize them, and that has security risks. + si.SetBoolValue("Main", "AutoLoadCheats", false); + + // No PCDRV or texture replacements, they require local files. + si.SetBoolValue("PCDrv", "Enabled", false); + si.SetBoolValue("TextureReplacements", "EnableVRAMWriteReplacements", false); + si.SetBoolValue("CDROM", "LoadImagePatches", false); + + // Disable achievements for now, we might be able to support them later though. + si.SetBoolValue("Cheevos", "Enabled", false); + + // Settings from host. +#define SELECT_SETTING(field) (msg ? msg->settings.field : g_settings.field) + si.SetStringValue("Console", "Region", + Settings::GetConsoleRegionName(msg ? msg->settings.console_region : System::GetRegion())); + si.SetStringValue("CPU", "ExecutionMode", Settings::GetCPUExecutionModeName(SELECT_SETTING(cpu_execution_mode))); + si.SetBoolValue("CPU", "OverclockEnable", SELECT_SETTING(cpu_overclock_enable)); + si.SetIntValue("CPU", "OverclockNumerator", SELECT_SETTING(cpu_overclock_numerator)); + si.SetIntValue("CPU", "OverclockDenominator", SELECT_SETTING(cpu_overclock_denominator)); + si.SetBoolValue("CPU", "RecompilerMemoryExceptions", SELECT_SETTING(cpu_recompiler_memory_exceptions)); + si.SetBoolValue("CPU", "RecompilerICache", SELECT_SETTING(cpu_recompiler_icache)); + si.SetBoolValue("GPU", "DisableInterlacing", SELECT_SETTING(gpu_disable_interlacing)); + si.SetBoolValue("GPU", "ForceNTSCTimings", SELECT_SETTING(gpu_force_ntsc_timings)); + si.SetBoolValue("GPU", "WidescreenHack", SELECT_SETTING(gpu_widescreen_hack)); + si.SetBoolValue("GPU", "PGXPEnable", SELECT_SETTING(gpu_pgxp_enable)); + si.SetBoolValue("GPU", "PGXPCulling", SELECT_SETTING(gpu_pgxp_culling)); + si.SetBoolValue("GPU", "PGXPCPU", SELECT_SETTING(gpu_pgxp_cpu)); + si.SetBoolValue("GPU", "PGXPPreserveProjFP", SELECT_SETTING(gpu_pgxp_preserve_proj_fp)); + si.SetBoolValue("CDROM", "RegionCheck", SELECT_SETTING(cdrom_region_check)); + si.SetBoolValue("Main", "DisableAllEnhancements", SELECT_SETTING(disable_all_enhancements)); + si.SetBoolValue("Hacks", "UseOldMDECRoutines", SELECT_SETTING(use_old_mdec_routines)); + si.SetBoolValue("BIOS", "PatchTTYEnable", SELECT_SETTING(bios_patch_tty_enable)); + si.SetBoolValue("BIOS", "PatchFastBoot", msg ? msg->settings.was_fast_booted : System::WasFastBooted()); + si.SetBoolValue("Console", "Enable8MBRAM", SELECT_SETTING(enable_8mb_ram)); + si.SetStringValue( + "Display", "AspectRatio", + Settings::GetDisplayAspectRatioName(msg ? msg->settings.display_aspect_ratio : + (g_settings.display_aspect_ratio == DisplayAspectRatio::MatchWindow ? + DisplayAspectRatio::Auto : + g_settings.display_aspect_ratio))); + si.SetIntValue("Display", "CustomAspectRatioNumerator", SELECT_SETTING(display_aspect_ratio_custom_numerator)); + si.GetIntValue("Display", "CustomAspectRatioDenominator", SELECT_SETTING(display_aspect_ratio_custom_denominator)); + si.SetStringValue("ControllerPorts", "MultitapMode", Settings::GetMultitapModeName(SELECT_SETTING(multitap_mode))); + si.SetIntValue("Hacks", "DMAMaxSliceTicks", SELECT_SETTING(dma_max_slice_ticks)); + si.SetIntValue("Hacks", "DMAHaltTicks", SELECT_SETTING(dma_halt_ticks)); + si.SetIntValue("Hacks", "GPUFIFOSize", SELECT_SETTING(gpu_fifo_size)); + si.SetIntValue("Hacks", "GPUMaxRunAhead", SELECT_SETTING(gpu_max_run_ahead)); +#undef SELECT_SETTING Host::Internal::SetNetplaySettingsLayer(&si); System::ApplySettings(false); } +void Netplay::FillSettings(ConnectResponseMessage* msg) +{ +#define FILL_SETTING(field) msg->settings.field = g_settings.field + msg->settings.console_region = System::GetRegion(); + FILL_SETTING(cpu_execution_mode); + FILL_SETTING(cpu_overclock_enable); + FILL_SETTING(cpu_overclock_numerator); + FILL_SETTING(cpu_overclock_denominator); + FILL_SETTING(cpu_recompiler_memory_exceptions); + FILL_SETTING(cpu_recompiler_icache); + FILL_SETTING(gpu_disable_interlacing); + FILL_SETTING(gpu_force_ntsc_timings); + FILL_SETTING(gpu_widescreen_hack); + FILL_SETTING(gpu_pgxp_enable); + FILL_SETTING(gpu_pgxp_culling); + FILL_SETTING(gpu_pgxp_cpu); + FILL_SETTING(gpu_pgxp_preserve_proj_fp); + FILL_SETTING(cdrom_region_check); + FILL_SETTING(disable_all_enhancements); + FILL_SETTING(use_old_mdec_routines); + FILL_SETTING(bios_patch_tty_enable); + msg->settings.was_fast_booted = System::WasFastBooted(); + FILL_SETTING(enable_8mb_ram); + FILL_SETTING(display_aspect_ratio); + FILL_SETTING(display_aspect_ratio_custom_numerator); + FILL_SETTING(display_aspect_ratio_custom_denominator); + FILL_SETTING(multitap_mode); + FILL_SETTING(dma_max_slice_ticks); + FILL_SETTING(dma_halt_ticks); + FILL_SETTING(gpu_fifo_size); + FILL_SETTING(gpu_max_run_ahead); +#undef FILL_SETTING +} + ////////////////////////////////////////////////////////////////////////// // Frame Pacing ////////////////////////////////////////////////////////////////////////// @@ -1801,7 +1908,8 @@ void Netplay::NetplayAdvanceFrame(Netplay::Input inputs[], int disconnect_flags) void Netplay::ExecuteNetplay() { // TODO: Fix this hackery to get out of the CPU loop... - System::SetState(System::State::Running); + if (System::IsValid()) + System::SetState(System::State::Running); while (s_state != SessionState::Inactive) { diff --git a/src/core/netplay_packets.h b/src/core/netplay_packets.h index d53733ecf..5d8261687 100644 --- a/src/core/netplay_packets.h +++ b/src/core/netplay_packets.h @@ -64,14 +64,50 @@ struct ConnectResponseMessage u64 game_hash; u32 game_serial_length; u32 game_title_length; - ConsoleRegion console_region; BIOS::Hash bios_hash; - bool was_fast_booted; + + struct + { + ConsoleRegion console_region; + CPUExecutionMode cpu_execution_mode; + u32 cpu_overclock_numerator; + u32 cpu_overclock_denominator; + bool cpu_overclock_enable; + bool cpu_recompiler_memory_exceptions; + bool cpu_recompiler_icache; + bool gpu_disable_interlacing; + bool gpu_force_ntsc_timings; + bool gpu_widescreen_hack; + bool gpu_pgxp_enable; + bool gpu_pgxp_culling; + bool gpu_pgxp_cpu; + bool gpu_pgxp_preserve_proj_fp; + bool cdrom_region_check; + bool disable_all_enhancements; + bool use_old_mdec_routines; + bool bios_patch_tty_enable; + bool was_fast_booted; + bool enable_8mb_ram; + DisplayAspectRatio display_aspect_ratio; + u16 display_aspect_ratio_custom_numerator; + u16 display_aspect_ratio_custom_denominator; + MultitapMode multitap_mode; + TickCount dma_max_slice_ticks; + TickCount dma_halt_ticks; + u32 gpu_fifo_size; + TickCount gpu_max_run_ahead; + } settings; // * game_serial_length + game_title_length follows // TODO: Include the settings overlays required to match the host config. - bool Validate() const { return static_cast(console_region) < static_cast(ConsoleRegion::Count); } + bool Validate() const + { + return (static_cast(settings.console_region) < static_cast(ConsoleRegion::Count) && + static_cast(settings.cpu_execution_mode) < static_cast(CPUExecutionMode::Count) && + static_cast(settings.display_aspect_ratio) < static_cast(DisplayAspectRatio::Count) && + static_cast(settings.multitap_mode) < static_cast(MultitapMode::Count)); + } std::string_view GetGameSerial() const { diff --git a/src/core/system.cpp b/src/core/system.cpp index 28cf52f1f..97ce0a10f 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -87,7 +87,7 @@ static bool ReadExecutableFromImage(ISOReader& iso, std::string* out_executable_ static void StallCPU(TickCount ticks); -static bool LoadBIOS(); +static bool LoadBIOS(const std::string& override_bios_path); static void InternalReset(); static void ClearRunningGame(); static void DestroySystem(); @@ -1251,7 +1251,7 @@ bool System::BootSystem(SystemBootParameters parameters) #endif // Load BIOS image. - if (!LoadBIOS()) + if (!LoadBIOS(parameters.override_bios)) { s_state = State::Shutdown; ClearRunningGame(); @@ -1364,44 +1364,6 @@ bool System::BootSystem(SystemBootParameters parameters) return true; } -bool System::ReinitializeSystem(ConsoleRegion region, const char* bios_path, const char* media_path, bool fast_boot) -{ - std::optional bios_image = FileSystem::ReadBinaryFile(bios_path); - if (!bios_image.has_value()) - { - Log_ErrorPrintf("Failed to read replacement BIOS at '%s'", bios_path); - return false; - } - - if (!InsertMedia(media_path)) - { - Log_ErrorPrintf("Failed to insert media at '%s'", media_path); - return false; - } - - // Replace the BIOS. - s_bios_hash = BIOS::GetImageHash(bios_image.value()); - s_bios_image_info = BIOS::GetInfoForImage(bios_image.value(), s_bios_hash); - if (s_bios_image_info) - Log_InfoPrintf("Replacing BIOS: %s", s_bios_image_info->description); - else - Log_WarningPrintf("Replacing with an unknown BIOS: %s", s_bios_hash.ToString().c_str()); - - std::memcpy(Bus::g_bios, bios_image->data(), Bus::BIOS_SIZE); - - if (s_bios_image_info && s_bios_image_info->patch_compatible) - BIOS::PatchBIOSEnableTTY(Bus::g_bios, Bus::BIOS_SIZE); - - s_was_fast_booted = false; - if (s_bios_image_info && s_bios_image_info->patch_compatible && fast_boot) - { - BIOS::PatchBIOSFastBoot(Bus::g_bios, Bus::BIOS_SIZE); - s_was_fast_booted = true; - } - - return true; -} - bool System::Initialize(bool force_software_renderer) { g_ticks_per_second = ScaleTicksToOverclock(MASTER_CLOCK); @@ -1843,22 +1805,6 @@ bool System::DoState(StateWrapper& sw, GPUTexture** host_texture, bool update_di Settings::CPUOverclockFractionToPercent(cpu_overclock_numerator, cpu_overclock_denominator) : 100u); - // during netplay if a file savestate is loaded set - // the overclocks to the same value as the savestate - // file savestates are usually only loaded at game start - if (Netplay::IsActive() && !is_memory_state && cpu_overclock_active) - { - g_settings.cpu_overclock_enable = cpu_overclock_active; - g_settings.cpu_overclock_numerator = cpu_overclock_numerator; - g_settings.cpu_overclock_denominator = cpu_overclock_denominator; - - g_settings.UpdateOverclockActive(); - - Host::AddFormattedOSDMessage(10.0f, - Host::TranslateString("OSDMessage", "WARNING: CPU overclock was changed to (%u%%) to match netplay savestate."), - g_settings.cpu_overclock_enable ? g_settings.GetCPUOverclockPercent() : 100u ); - } - UpdateOverclock(); } @@ -1892,9 +1838,10 @@ bool System::DoState(StateWrapper& sw, GPUTexture** host_texture, bool update_di return !sw.HasError(); } -bool System::LoadBIOS() +bool System::LoadBIOS(const std::string& override_bios_path) { - std::optional bios_image(BIOS::GetBIOSImage(s_region)); + std::optional bios_image( + override_bios_path.empty() ? BIOS::GetBIOSImage(s_region) : FileSystem::ReadBinaryFile(override_bios_path.c_str())); if (!bios_image.has_value()) { Host::ReportFormattedErrorAsync("Error", Host::TranslateString("System", "Failed to load %s BIOS."), diff --git a/src/core/system.h b/src/core/system.h index 21f565746..168b821b9 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -39,6 +39,7 @@ struct SystemBootParameters std::string filename; std::string save_state; std::string override_exe; + std::string override_bios; std::optional override_fast_boot; std::optional override_fullscreen; std::optional override_start_paused; @@ -223,7 +224,6 @@ void ApplySettings(bool display_osd_messages); bool ReloadGameSettings(bool display_osd_messages); bool BootSystem(SystemBootParameters parameters); -bool ReinitializeSystem(ConsoleRegion region, const char* bios_path, const char* media_path, bool fast_boot); void PauseSystem(bool paused); void ResetSystem(); diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 384c8713f..843b25791 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -408,7 +408,9 @@ void EmuThread::applySettings(bool display_osd_messages /* = false */) } System::ApplySettings(display_osd_messages); - if (!FullscreenUI::IsInitialized() && System::IsPaused()) + if (!FullscreenUI::IsInitialized() && !System::IsValid()) + setInitialState(std::nullopt); + else if (!FullscreenUI::IsInitialized() && System::IsPaused()) redrawDisplayWindow(); } @@ -1104,6 +1106,9 @@ void EmuThread::joinNetplaySession(const QString& nickname, const QString& hostn errorReported(tr("Netplay Error"), tr("Failed to join netplay session. The log may contain more information.")); return; } + + // Exit the event loop, we'll take it from here. + g_emu_thread->wakeThread(); } void EmuThread::runOnEmuThread(std::function callback) @@ -1451,11 +1456,12 @@ void EmuThread::run() // bind buttons/axises createBackgroundControllerPollTimer(); startBackgroundControllerPollTimer(); + setInitialState(std::nullopt); // main loop while (!m_shutdown_flag) { - if (Netplay::IsActive() && System::IsValid()) + if (Netplay::IsActive()) { Netplay::ExecuteNetplay(); }