diff --git a/Source/Core/Core/BootManager.cpp b/Source/Core/Core/BootManager.cpp index de35120c4a..7f7130ce66 100644 --- a/Source/Core/Core/BootManager.cpp +++ b/Source/Core/Core/BootManager.cpp @@ -372,6 +372,8 @@ bool BootCore(std::unique_ptr boot) StartUp.bMMU = netplay_settings.m_MMU; StartUp.bFastmem = netplay_settings.m_Fastmem; StartUp.bHLE_BS2 = netplay_settings.m_SkipIPL; + if (netplay_settings.m_HostInputAuthority && !netplay_settings.m_IsHosting) + config_cache.bSetEmulationSpeed = true; } else { diff --git a/Source/Core/Core/HW/SI/SI.cpp b/Source/Core/Core/HW/SI/SI.cpp index 5e1fa38993..ff06a4cfc1 100644 --- a/Source/Core/Core/HW/SI/SI.cpp +++ b/Source/Core/Core/HW/SI/SI.cpp @@ -595,6 +595,10 @@ void ChangeDeviceDeterministic(SIDevices device, int channel) void UpdateDevices() { + // Hinting NetPlay that all controllers will be polled in + // succession, in order to optimize networking + NetPlay::SetSIPollBatching(true); + // Update inputs at the rate of SI // Typically 120hz but is variable g_controller_interface.UpdateInput(); @@ -610,6 +614,9 @@ void UpdateDevices() !!s_channel[3].device->GetData(s_channel[3].in_hi.hex, s_channel[3].in_lo.hex); UpdateInterrupts(); + + // Polling finished + NetPlay::SetSIPollBatching(false); } SIDevices GetDeviceType(int channel) diff --git a/Source/Core/Core/NetPlayClient.cpp b/Source/Core/Core/NetPlayClient.cpp index a7016a2665..2f091ae2b4 100644 --- a/Source/Core/Core/NetPlayClient.cpp +++ b/Source/Core/Core/NetPlayClient.cpp @@ -60,6 +60,7 @@ namespace NetPlay static std::mutex crit_netplay_client; static NetPlayClient* netplay_client = nullptr; static std::unique_ptr s_wii_sync_fs; +static bool s_si_poll_batching; // called from ---GUI--- thread NetPlayClient::~NetPlayClient() @@ -407,6 +408,22 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) } break; + case NP_MSG_PAD_FIRST_RECEIVED: + { + PadMapping map; + packet >> map; + packet >> m_first_pad_status_received[map]; + m_first_pad_status_received_event.Set(); + } + break; + + case NP_MSG_HOST_INPUT_AUTHORITY: + { + packet >> m_host_input_authority; + m_dialog->OnHostInputAuthorityChanged(m_host_input_authority); + } + break; + case NP_MSG_CHANGE_GAME: { { @@ -532,7 +549,9 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) packet >> m_net_settings.m_SyncSaveData; packet >> m_net_settings.m_SaveDataRegion; - m_net_settings.m_IsHosting = m_dialog->IsHosting(); + + m_net_settings.m_IsHosting = m_local_player->IsHost(); + m_net_settings.m_HostInputAuthority = m_host_input_authority; } m_dialog->OnMsgStartGame(); @@ -636,7 +655,7 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) case SYNC_SAVE_DATA_RAW: { - if (m_dialog->IsHosting()) + if (m_local_player->IsHost()) return 0; bool is_slot_a; @@ -660,7 +679,7 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) case SYNC_SAVE_DATA_GCI: { - if (m_dialog->IsHosting()) + if (m_local_player->IsHost()) return 0; bool is_slot_a; @@ -696,7 +715,7 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) case SYNC_SAVE_DATA_WII: { - if (m_dialog->IsHosting()) + if (m_local_player->IsHost()) return 0; const auto game = m_dialog->FindGameFile(m_selected_game); @@ -1118,6 +1137,8 @@ bool NetPlayClient::StartGame(const std::string& path) ClearBuffers(); + m_first_pad_status_received.fill(false); + if (m_dialog->IsRecording()) { if (Movie::IsReadOnly()) @@ -1366,7 +1387,7 @@ void NetPlayClient::OnConnectFailed(u8 reason) } // called from ---CPU--- thread -bool NetPlayClient::GetNetPads(const int pad_nb, GCPadStatus* pad_status) +bool NetPlayClient::GetNetPads(const int pad_nb, const bool batching, GCPadStatus* pad_status) { // The interface for this is extremely silly. // @@ -1385,11 +1406,17 @@ bool NetPlayClient::GetNetPads(const int pad_nb, GCPadStatus* pad_status) // The slot number is the "local" pad number, and what player // it actually means is the "in-game" pad number. - // When the 1st in-game pad is polled, we assume the others will - // will be polled as well. To reduce latency, we poll all local - // controllers at once and then send the status to the other + // When the 1st in-game pad is polled and batching is set, the + // others will be polled as well. To reduce latency, we poll all + // local controllers at once and then send the status to the other // clients. - if (IsFirstInGamePad(pad_nb)) + // + // Batching is enabled when polled from VI. If batching is not + // enabled, the poll is probably from MMIO, which can poll any + // specific pad arbitrarily. In this case, we poll just that pad + // and send it. + + if (IsFirstInGamePad(pad_nb) && batching) { sf::Packet packet; packet << static_cast(NP_MSG_PAD_DATA); @@ -1398,34 +1425,44 @@ bool NetPlayClient::GetNetPads(const int pad_nb, GCPadStatus* pad_status) const int num_local_pads = NumLocalPads(); for (int local_pad = 0; local_pad < num_local_pads; local_pad++) { - switch (SConfig::GetInstance().m_SIDevice[local_pad]) - { - case SerialInterface::SIDEVICE_WIIU_ADAPTER: - *pad_status = GCAdapter::Input(local_pad); - break; - case SerialInterface::SIDEVICE_GC_CONTROLLER: - default: - *pad_status = Pad::GetStatus(local_pad); - break; - } - - int ingame_pad = LocalPadToInGamePad(local_pad); - - // adjust the buffer either up or down - // inserting multiple padstates or dropping states - while (m_pad_buffer[ingame_pad].Size() <= m_target_buffer_size) - { - // add to buffer - m_pad_buffer[ingame_pad].Push(*pad_status); - - // add to packet - AddPadStateToPacket(ingame_pad, *pad_status, packet); - send_packet = true; - } + send_packet = PollLocalPad(local_pad, packet) || send_packet; } if (send_packet) SendAsync(std::move(packet)); + + if (m_host_input_authority) + SendPadHostPoll(-1); + } + + if (!batching) + { + int local_pad = InGamePadToLocalPad(pad_nb); + if (local_pad < 4) + { + sf::Packet packet; + packet << static_cast(NP_MSG_PAD_DATA); + if (PollLocalPad(local_pad, packet)) + SendAsync(std::move(packet)); + } + + if (m_host_input_authority) + SendPadHostPoll(pad_nb); + } + + if (m_host_input_authority && !m_local_player->IsHost()) + { + const bool buffer_over_target = m_pad_buffer[pad_nb].Size() > m_target_buffer_size + 1; + if (!buffer_over_target) + m_buffer_under_target_last = std::chrono::steady_clock::now(); + + std::chrono::duration time_diff = + std::chrono::steady_clock::now() - m_buffer_under_target_last; + if (time_diff.count() >= 1.0 || !buffer_over_target) + { + // run fast if the buffer is overfilled, otherwise run normal speed + SConfig::GetInstance().m_EmulationSpeed = buffer_over_target ? 0.0f : 1.0f; + } } // Now, we either use the data pushed earlier, or wait for the @@ -1534,6 +1571,86 @@ bool NetPlayClient::WiimoteUpdate(int _number, u8* data, const u8 size, u8 repor return true; } +bool NetPlayClient::PollLocalPad(const int local_pad, sf::Packet& packet) +{ + GCPadStatus pad_status; + + switch (SConfig::GetInstance().m_SIDevice[local_pad]) + { + case SerialInterface::SIDEVICE_WIIU_ADAPTER: + pad_status = GCAdapter::Input(local_pad); + break; + case SerialInterface::SIDEVICE_GC_CONTROLLER: + default: + pad_status = Pad::GetStatus(local_pad); + break; + } + + const int ingame_pad = LocalPadToInGamePad(local_pad); + bool data_added = false; + + if (m_host_input_authority) + { + // add to packet + AddPadStateToPacket(ingame_pad, pad_status, packet); + data_added = true; + } + else + { + // adjust the buffer either up or down + // inserting multiple padstates or dropping states + while (m_pad_buffer[ingame_pad].Size() <= m_target_buffer_size) + { + // add to buffer + m_pad_buffer[ingame_pad].Push(pad_status); + + // add to packet + AddPadStateToPacket(ingame_pad, pad_status, packet); + data_added = true; + } + } + + return data_added; +} + +void NetPlayClient::SendPadHostPoll(const PadMapping pad_num) +{ + if (!m_local_player->IsHost()) + return; + + if (pad_num < 0) + { + for (size_t i = 0; i < m_pad_map.size(); i++) + { + if (m_pad_map[i] <= 0) + continue; + + while (!m_first_pad_status_received[i]) + { + if (!m_is_running.IsSet()) + return; + + m_first_pad_status_received_event.Wait(); + } + } + } + else if (m_pad_map[pad_num] > 0) + { + while (!m_first_pad_status_received[pad_num]) + { + if (!m_is_running.IsSet()) + return; + + m_first_pad_status_received_event.Wait(); + } + } + + sf::Packet packet; + packet << static_cast(NP_MSG_PAD_HOST_POLL); + packet << pad_num; + SendAsync(std::move(packet)); +} + // called from ---GUI--- thread and ---NETPLAY--- thread (client side) bool NetPlayClient::StopGame() { @@ -1542,6 +1659,7 @@ bool NetPlayClient::StopGame() // stop waiting for input m_gc_pad_event.Set(); m_wii_pad_event.Set(); + m_first_pad_status_received_event.Set(); NetPlay_Disable(); @@ -1564,6 +1682,7 @@ void NetPlayClient::Stop() // stop waiting for input m_gc_pad_event.Set(); m_wii_pad_event.Set(); + m_first_pad_status_received_event.Set(); // Tell the server to stop if we have a pad mapped in game. if (LocalPlayerHasControllerMapped()) @@ -1719,6 +1838,12 @@ const PadMappingArray& NetPlayClient::GetWiimoteMapping() const return m_wiimote_map; } +void NetPlayClient::AdjustPadBufferSize(const unsigned int size) +{ + m_target_buffer_size = size; + m_dialog->OnPadBufferChanged(size); +} + bool IsNetPlayRunning() { return netplay_client != nullptr; @@ -1750,6 +1875,11 @@ void ClearWiiSyncFS() s_wii_sync_fs.reset(); } +void SetSIPollBatching(bool state) +{ + s_si_poll_batching = state; +} + void NetPlay_Enable(NetPlayClient* const np) { std::lock_guard lk(crit_netplay_client); @@ -1767,12 +1897,12 @@ void NetPlay_Disable() // called from ---CPU--- thread // Actual Core function which is called on every frame -bool SerialInterface::CSIDevice_GCController::NetPlay_GetInput(int numPAD, GCPadStatus* PadStatus) +bool SerialInterface::CSIDevice_GCController::NetPlay_GetInput(int pad_num, GCPadStatus* status) { std::lock_guard lk(NetPlay::crit_netplay_client); if (NetPlay::netplay_client) - return NetPlay::netplay_client->GetNetPads(numPAD, PadStatus); + return NetPlay::netplay_client->GetNetPads(pad_num, NetPlay::s_si_poll_batching, status); return false; } diff --git a/Source/Core/Core/NetPlayClient.h b/Source/Core/Core/NetPlayClient.h index aa8b2f639f..1b0601452a 100644 --- a/Source/Core/Core/NetPlayClient.h +++ b/Source/Core/Core/NetPlayClient.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -43,6 +44,7 @@ public: virtual void OnMsgStartGame() = 0; virtual void OnMsgStopGame() = 0; virtual void OnPadBufferChanged(u32 buffer) = 0; + virtual void OnHostInputAuthorityChanged(bool enabled) = 0; virtual void OnDesync(u32 frame, const std::string& player) = 0; virtual void OnConnectionLost() = 0; virtual void OnConnectionError(const std::string& message) = 0; @@ -74,6 +76,8 @@ public: std::string revision; u32 ping; PlayerGameStatus game_status; + + bool IsHost() const { return pid == 1; } }; class NetPlayClient : public TraversalClientClient @@ -101,7 +105,7 @@ public: // Send and receive pads values bool WiimoteUpdate(int _number, u8* data, const u8 size, u8 reporting_mode); - bool GetNetPads(int pad_nb, GCPadStatus* pad_status); + bool GetNetPads(int pad_nb, bool from_vi, GCPadStatus* pad_status); u64 GetInitialRTCValue() const; @@ -121,6 +125,8 @@ public: const PadMappingArray& GetPadMapping() const; const PadMappingArray& GetWiimoteMapping() const; + void AdjustPadBufferSize(unsigned int size); + protected: void ClearBuffers(); @@ -137,6 +143,10 @@ protected: std::array, 4> m_pad_buffer; std::array, 4> m_wiimote_buffer; + std::array m_first_pad_status_received{}; + + std::chrono::time_point m_buffer_under_target_last; + NetPlayUI* m_dialog = nullptr; ENetHost* m_client = nullptr; @@ -148,6 +158,7 @@ protected: Common::Flag m_do_loop{true}; unsigned int m_target_buffer_size = 20; + bool m_host_input_authority = false; Player* m_local_player = nullptr; @@ -178,6 +189,9 @@ private: bool DecompressPacketIntoFile(sf::Packet& packet, const std::string& file_path); std::optional> DecompressPacketIntoBuffer(sf::Packet& packet); + bool PollLocalPad(int local_pad, sf::Packet& packet); + void SendPadHostPoll(PadMapping pad_num); + void UpdateDevices(); void AddPadStateToPacket(int in_game_pad, const GCPadStatus& np, sf::Packet& packet); void SendWiimoteState(int in_game_pad, const NetWiimote& nw); @@ -203,6 +217,7 @@ private: bool m_should_compute_MD5 = false; Common::Event m_gc_pad_event; Common::Event m_wii_pad_event; + Common::Event m_first_pad_status_received_event; u8 m_sync_save_data_count = 0; u8 m_sync_save_data_success_count = 0; diff --git a/Source/Core/Core/NetPlayProto.h b/Source/Core/Core/NetPlayProto.h index 3f643b9750..8e175de807 100644 --- a/Source/Core/Core/NetPlayProto.h +++ b/Source/Core/Core/NetPlayProto.h @@ -78,6 +78,7 @@ struct NetSettings bool m_SyncSaveData; std::string m_SaveDataRegion; bool m_IsHosting; + bool m_HostInputAuthority; }; struct NetTraversalConfig @@ -110,6 +111,8 @@ enum NP_MSG_PAD_DATA = 0x60, NP_MSG_PAD_MAPPING = 0x61, NP_MSG_PAD_BUFFER = 0x62, + NP_MSG_PAD_HOST_POLL = 0x63, + NP_MSG_PAD_FIRST_RECEIVED = 0x64, NP_MSG_WIIMOTE_DATA = 0x70, NP_MSG_WIIMOTE_MAPPING = 0x71, @@ -120,6 +123,7 @@ enum NP_MSG_DISABLE_GAME = 0xA3, NP_MSG_GAME_STATUS = 0xA4, NP_MSG_IPL_STATUS = 0xA5, + NP_MSG_HOST_INPUT_AUTHORITY = 0xA6, NP_MSG_TIMEBASE = 0xB0, NP_MSG_DESYNC_DETECTED = 0xB1, @@ -175,4 +179,5 @@ const NetSettings& GetNetSettings(); IOS::HLE::FS::FileSystem* GetWiiSyncFS(); void SetWiiSyncFS(std::unique_ptr fs); void ClearWiiSyncFS(); +void SetSIPollBatching(bool state); } // namespace NetPlay diff --git a/Source/Core/Core/NetPlayServer.cpp b/Source/Core/Core/NetPlayServer.cpp index abf0a63fe8..18bf6eb378 100644 --- a/Source/Core/Core/NetPlayServer.cpp +++ b/Source/Core/Core/NetPlayServer.cpp @@ -307,13 +307,13 @@ unsigned int NetPlayServer::OnConnect(ENetPeer* socket) // send join message to already connected clients sf::Packet spac; - spac << (MessageId)NP_MSG_PLAYER_JOIN; + spac << static_cast(NP_MSG_PLAYER_JOIN); spac << player.pid << player.name << player.revision; SendToClients(spac); // send new client success message with their id spac.clear(); - spac << (MessageId)0; + spac << static_cast(0); spac << player.pid; Send(player.socket, spac); @@ -321,15 +321,24 @@ unsigned int NetPlayServer::OnConnect(ENetPeer* socket) if (m_selected_game != "") { spac.clear(); - spac << (MessageId)NP_MSG_CHANGE_GAME; + spac << static_cast(NP_MSG_CHANGE_GAME); spac << m_selected_game; Send(player.socket, spac); } - // send the pad buffer value + if (!m_host_input_authority) + { + // send the pad buffer value + spac.clear(); + spac << static_cast(NP_MSG_PAD_BUFFER); + spac << static_cast(m_target_buffer_size); + Send(player.socket, spac); + } + + // send input authority state spac.clear(); - spac << (MessageId)NP_MSG_PAD_BUFFER; - spac << (u32)m_target_buffer_size; + spac << static_cast(NP_MSG_HOST_INPUT_AUTHORITY); + spac << m_host_input_authority; Send(player.socket, spac); // sync GC SRAM with new client @@ -340,7 +349,7 @@ unsigned int NetPlayServer::OnConnect(ENetPeer* socket) g_SRAM_netplay_initialized = true; } spac.clear(); - spac << (MessageId)NP_MSG_SYNC_GC_SRAM; + spac << static_cast(NP_MSG_SYNC_GC_SRAM); for (size_t i = 0; i < sizeof(g_SRAM.p_SRAM); ++i) { spac << g_SRAM.p_SRAM[i]; @@ -497,6 +506,24 @@ void NetPlayServer::AdjustPadBufferSize(unsigned int size) SendAsyncToClients(std::move(spac)); } +void NetPlayServer::SetHostInputAuthority(const bool enable) +{ + std::lock_guard lkg(m_crit.game); + + m_host_input_authority = enable; + + // tell clients about the new value + sf::Packet spac; + spac << static_cast(NP_MSG_HOST_INPUT_AUTHORITY); + spac << m_host_input_authority; + + SendAsyncToClients(std::move(spac)); + + // resend pad buffer to clients when disabled + if (!m_host_input_authority) + AdjustPadBufferSize(m_target_buffer_size); +} + void NetPlayServer::SendAsyncToClients(sf::Packet&& packet) { { @@ -559,13 +586,59 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, Client& player) packet >> pad.button >> pad.analogA >> pad.analogB >> pad.stickX >> pad.stickY >> pad.substickX >> pad.substickY >> pad.triggerLeft >> pad.triggerRight >> pad.isConnected; - // Add to packet for relay to clients - spac << map << pad.button << pad.analogA << pad.analogB << pad.stickX << pad.stickY + if (m_host_input_authority) + { + m_last_pad_status[map] = pad; + + if (!m_first_pad_status_received[map]) + { + m_first_pad_status_received[map] = true; + SendFirstReceivedToHost(map, true); + } + } + else + { + spac << map << pad.button << pad.analogA << pad.analogB << pad.stickX << pad.stickY + << pad.substickX << pad.substickY << pad.triggerLeft << pad.triggerRight + << pad.isConnected; + } + } + + if (!m_host_input_authority) + SendToClients(spac, player.pid); + } + break; + + case NP_MSG_PAD_HOST_POLL: + { + PadMapping pad_num; + packet >> pad_num; + + sf::Packet spac; + spac << static_cast(NP_MSG_PAD_DATA); + + if (pad_num < 0) + { + for (size_t i = 0; i < m_pad_map.size(); i++) + { + if (m_pad_map[i] == -1) + continue; + + const GCPadStatus& pad = m_last_pad_status[i]; + spac << static_cast(i) << pad.button << pad.analogA << pad.analogB << pad.stickX + << pad.stickY << pad.substickX << pad.substickY << pad.triggerLeft << pad.triggerRight + << pad.isConnected; + } + } + else if (m_pad_map.at(pad_num) != -1) + { + const GCPadStatus& pad = m_last_pad_status[pad_num]; + spac << pad_num << pad.button << pad.analogA << pad.analogB << pad.stickX << pad.stickY << pad.substickX << pad.substickY << pad.triggerLeft << pad.triggerRight << pad.isConnected; } - SendToClients(spac, player.pid); + SendToClients(spac); } break; @@ -911,7 +984,10 @@ bool NetPlayServer::StartGame() m_current_game = Common::Timer::GetTimeMs(); // no change, just update with clients - AdjustPadBufferSize(m_target_buffer_size); + if (!m_host_input_authority) + AdjustPadBufferSize(m_target_buffer_size); + + m_first_pad_status_received.fill(false); const u64 initial_rtc = GetInitialNetPlayRTC(); @@ -1291,6 +1367,15 @@ bool NetPlayServer::CompressBufferIntoPacket(const std::vector& in_buffer, s return true; } +void NetPlayServer::SendFirstReceivedToHost(const PadMapping map, const bool state) +{ + sf::Packet pac; + pac << static_cast(NP_MSG_PAD_FIRST_RECEIVED); + pac << map; + pac << state; + Send(m_players.at(1).socket, pac); +} + u64 NetPlayServer::GetInitialNetPlayRTC() const { const auto& config = SConfig::GetInstance(); diff --git a/Source/Core/Core/NetPlayServer.h b/Source/Core/Core/NetPlayServer.h index 7dbc384a61..a0c49dd04f 100644 --- a/Source/Core/Core/NetPlayServer.h +++ b/Source/Core/Core/NetPlayServer.h @@ -18,6 +18,7 @@ #include "Common/Timer.h" #include "Common/TraversalClient.h" #include "Core/NetPlayProto.h" +#include "InputCommon/GCPadStatus.h" namespace NetPlay { @@ -51,6 +52,7 @@ public: void SetWiimoteMapping(const PadMappingArray& mappings); void AdjustPadBufferSize(unsigned int size); + void SetHostInputAuthority(bool enable); void KickPlayer(PlayerId player); @@ -79,11 +81,13 @@ private: Common::QoSSession qos_session; bool operator==(const Client& other) const { return this == &other; } + bool IsHost() const { return pid == 1; } }; bool SyncSaveData(); bool CompressFileIntoPacket(const std::string& file_path, sf::Packet& packet); bool CompressBufferIntoPacket(const std::vector& in_buffer, sf::Packet& packet); + void SendFirstReceivedToHost(PadMapping map, bool state); u64 GetInitialNetPlayRTC() const; @@ -113,12 +117,16 @@ private: PadMappingArray m_wiimote_map; unsigned int m_save_data_synced_players = 0; bool m_start_pending = false; + bool m_host_input_authority = false; std::map m_players; std::unordered_map>> m_timebase_by_frame; bool m_desync_detected; + std::array m_last_pad_status{}; + std::array m_first_pad_status_received{}; + struct { std::recursive_mutex game; diff --git a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp index a6775180de..5edd279c9d 100644 --- a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp +++ b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -99,6 +100,7 @@ void NetPlayDialog::CreateMainLayout() m_record_input_box = new QCheckBox(tr("Record inputs")); m_reduce_polling_rate_box = new QCheckBox(tr("Reduce Polling Rate")); m_strict_settings_sync_box = new QCheckBox(tr("Strict Settings Sync")); + m_host_input_authority_box = new QCheckBox(tr("Host Input Authority")); m_buffer_label = new QLabel(tr("Buffer:")); m_quit_button = new QPushButton(tr("Quit")); m_splitter = new QSplitter(Qt::Horizontal); @@ -142,6 +144,14 @@ void NetPlayDialog::CreateMainLayout() tr("This will sync additional graphics settings, and force everyone to the same internal " "resolution.\nMay prevent desync in some games that use EFB reads. Please ensure everyone " "uses the same video backend.")); + m_host_input_authority_box->setToolTip( + tr("This gives the host control over when inputs are sent to the game, effectively " + "decoupling players from each other in terms of buffering.\nThis allows players to have " + "latency based solely on their connection to the host, rather than everyone's connection. " + "Buffer works differently\nin this mode. The host always has no latency, and the buffer " + "setting serves to prevent stutter, speeding up when the amount of buffered\ninputs " + "exceeds the set limit. Input delay is instead based on ping to the host. This results in " + "smoother gameplay on unstable connections.")); m_main_layout->addWidget(m_game_button, 0, 0); m_main_layout->addWidget(m_md5_button, 0, 1); @@ -163,6 +173,7 @@ void NetPlayDialog::CreateMainLayout() options_boxes->addWidget(m_record_input_box); options_boxes->addWidget(m_reduce_polling_rate_box); options_boxes->addWidget(m_strict_settings_sync_box); + options_boxes->addWidget(m_host_input_authority_box); options_widget->addLayout(options_boxes, 0, 3, Qt::AlignTop); options_widget->setColumnStretch(3, 1000); @@ -261,11 +272,20 @@ void NetPlayDialog::ConnectWidgets() if (value == m_buffer_size) return; + auto client = Settings::Instance().GetNetPlayClient(); auto server = Settings::Instance().GetNetPlayServer(); if (server) server->AdjustPadBufferSize(value); + else + client->AdjustPadBufferSize(value); }); + connect(m_host_input_authority_box, &QCheckBox::toggled, [this](bool checked) { + auto server = Settings::Instance().GetNetPlayServer(); + if (server) + server->SetHostInputAuthority(checked); + }); + connect(m_start_button, &QPushButton::clicked, this, &NetPlayDialog::OnStart); connect(m_quit_button, &QPushButton::clicked, this, &NetPlayDialog::reject); @@ -447,8 +467,7 @@ void NetPlayDialog::show(std::string nickname, bool use_traversal) m_sync_save_data_box->setHidden(!is_hosting); m_reduce_polling_rate_box->setHidden(!is_hosting); m_strict_settings_sync_box->setHidden(!is_hosting); - m_buffer_size_box->setHidden(!is_hosting); - m_buffer_label->setHidden(!is_hosting); + m_host_input_authority_box->setHidden(!is_hosting); m_kick_button->setHidden(!is_hosting); m_assign_ports_button->setHidden(!is_hosting); m_md5_button->setHidden(!is_hosting); @@ -458,6 +477,8 @@ void NetPlayDialog::show(std::string nickname, bool use_traversal) m_game_button->setEnabled(is_hosting); m_kick_button->setEnabled(false); + m_buffer_label->setText(is_hosting ? tr("Buffer:") : tr("Max Buffer:")); + QDialog::show(); UpdateGUI(); } @@ -720,6 +741,7 @@ void NetPlayDialog::SetOptionsEnabled(bool enabled) m_assign_ports_button->setEnabled(enabled); m_reduce_polling_rate_box->setEnabled(enabled); m_strict_settings_sync_box->setEnabled(enabled); + m_host_input_authority_box->setEnabled(enabled); } m_record_input_box->setEnabled(enabled); @@ -744,12 +766,51 @@ void NetPlayDialog::OnMsgStopGame() void NetPlayDialog::OnPadBufferChanged(u32 buffer) { - QueueOnObject(this, [this, buffer] { m_buffer_size_box->setValue(buffer); }); - DisplayMessage(tr("Buffer size changed to %1").arg(buffer), ""); + QueueOnObject(this, [this, buffer] { + const QSignalBlocker blocker(m_buffer_size_box); + m_buffer_size_box->setValue(buffer); + }); + DisplayMessage(m_host_input_authority && !IsHosting() ? + tr("Max buffer size changed to %1").arg(buffer) : + tr("Buffer size changed to %1").arg(buffer), + ""); m_buffer_size = static_cast(buffer); } +void NetPlayDialog::OnHostInputAuthorityChanged(bool enabled) +{ + QueueOnObject(this, [this, enabled] { + const bool is_hosting = IsHosting(); + const bool enable_buffer = is_hosting != enabled; + + if (is_hosting) + { + m_buffer_size_box->setEnabled(enable_buffer); + m_buffer_label->setEnabled(enable_buffer); + m_buffer_size_box->setHidden(false); + m_buffer_label->setHidden(false); + + QSignalBlocker blocker(m_host_input_authority_box); + m_host_input_authority_box->setChecked(enabled); + } + else + { + m_buffer_size_box->setEnabled(true); + m_buffer_label->setEnabled(true); + m_buffer_size_box->setHidden(!enable_buffer); + m_buffer_label->setHidden(!enable_buffer); + + if (enabled) + m_buffer_size_box->setValue(1); + } + }); + DisplayMessage(enabled ? tr("Host input authority enabled") : tr("Host input authority disabled"), + ""); + + m_host_input_authority = enabled; +} + void NetPlayDialog::OnDesync(u32 frame, const std::string& player) { DisplayMessage(tr("Possible desync detected: %1 might have desynced at frame %2") diff --git a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h index a9d835baf7..d666e8c33b 100644 --- a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h +++ b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h @@ -47,6 +47,7 @@ public: void OnMsgStartGame() override; void OnMsgStopGame() override; void OnPadBufferChanged(u32 buffer) override; + void OnHostInputAuthorityChanged(bool enabled) override; void OnDesync(u32 frame, const std::string& player) override; void OnConnectionLost() override; void OnConnectionError(const std::string& message) override; @@ -108,6 +109,7 @@ private: QCheckBox* m_record_input_box; QCheckBox* m_reduce_polling_rate_box; QCheckBox* m_strict_settings_sync_box; + QCheckBox* m_host_input_authority_box; QPushButton* m_quit_button; QSplitter* m_splitter; @@ -124,4 +126,5 @@ private: int m_buffer_size = 0; int m_player_count = 0; int m_old_player_count = 0; + bool m_host_input_authority = false; };