diff --git a/dep/ggpo-x/include/ggponet.h b/dep/ggpo-x/include/ggponet.h index 188dfcaba..8888d3372 100644 --- a/dep/ggpo-x/include/ggponet.h +++ b/dep/ggpo-x/include/ggponet.h @@ -196,8 +196,8 @@ typedef struct { } chat; struct { int nFrameOfDesync; - uint16_t ourCheckSum; - uint16_t remoteChecksum; + uint32_t ourCheckSum; + uint32_t remoteChecksum; } desync; } u; } GGPOEvent; @@ -248,7 +248,7 @@ typedef struct { * free_buffer - Frees a game state allocated in save_game_state. You * should deallocate the memory contained in the buffer. */ - void (__cdecl *free_buffer)(void* context, void *buffer); + void (__cdecl *free_buffer)(void* context, void *buffer, int frame); /* * advance_frame - Called during a rollback. You should advance your game diff --git a/dep/ggpo-x/src/backends/p2p.cpp b/dep/ggpo-x/src/backends/p2p.cpp index 210ae2e35..ce3fdcc58 100644 --- a/dep/ggpo-x/src/backends/p2p.cpp +++ b/dep/ggpo-x/src/backends/p2p.cpp @@ -428,21 +428,22 @@ GGPOErrorCode Peer2PeerBackend::SetManualNetworkPolling(bool value) GGPOErrorCode Peer2PeerBackend::IncrementFrame(uint16_t checksum1) -{ +{ auto currentFrame = _sync.GetFrameCount(); + _sync.IncrementFrame(); + uint32 cSum = _sync.GetLastSavedFrame().checksum; char buf[256]; - uint16_t cSum = checksum1; - Log("End of frame (%d)...\n", _sync.GetFrameCount()); + Log("End of frame (%d)...\n", currentFrame); static int maxDif = 0; - if (_pendingCheckSums.count(_sync.GetFrameCount())) + if (_pendingCheckSums.count(currentFrame)) { auto max = _pendingCheckSums.rbegin()->first; auto diff = max - currentFrame; maxDif = max(maxDif, diff); - int oldChecksum = _pendingCheckSums[_sync.GetFrameCount()]; - _pendingCheckSums[_sync.GetFrameCount()] = cSum; - sprintf_s<256>(buf, "Replace local checksum for frame %d: %d with %d, newest frame is %d, max diff %d\n", _sync.GetFrameCount(), oldChecksum, _pendingCheckSums[_sync.GetFrameCount()], max, maxDif); - + int oldChecksum = _pendingCheckSums[currentFrame]; + _pendingCheckSums[currentFrame] = cSum; + sprintf_s<256>(buf, "Replace local checksum for frame %d: %d with %d, newest frame is %d, max diff %d\n", + currentFrame, oldChecksum, _pendingCheckSums[currentFrame], max, maxDif); if (currentFrame <= _confirmedCheckSumFrame) { @@ -461,16 +462,13 @@ Peer2PeerBackend::IncrementFrame(uint16_t checksum1) } else { - sprintf_s<256>(buf, "Added local checksum for frame %d: %d\n", _sync.GetFrameCount(), cSum); + sprintf_s<256>(buf, "Added local checksum for frame %d: %d\n", currentFrame, cSum); //OutputDebugStringA(buf); } - _pendingCheckSums[_sync.GetFrameCount()]= cSum ; + _pendingCheckSums[currentFrame] = cSum; - - - _sync.IncrementFrame(); - DoPoll(); + DoPoll(); PollSyncEvents(); return GGPO_OK; @@ -511,7 +509,7 @@ Peer2PeerBackend::PollUdpProtocolEvents(void) //} } -void Peer2PeerBackend::CheckRemoteChecksum(int framenumber, uint16 cs) +void Peer2PeerBackend::CheckRemoteChecksum(int framenumber, uint32 cs) { if (framenumber <= _sync.MaxPredictionFrames()) return; @@ -522,7 +520,7 @@ void Peer2PeerBackend::CheckRemoteChecksum(int framenumber, uint16 cs) int Peer2PeerBackend::HowFarBackForChecksums()const { - return 16; + return _sync.MaxPredictionFrames() + 2; }/* uint16 Peer2PeerBackend::GetChecksumForConfirmedFrame(int frameNumber) const { diff --git a/dep/ggpo-x/src/backends/p2p.h b/dep/ggpo-x/src/backends/p2p.h index 100df7dc6..7835a0cb9 100644 --- a/dep/ggpo-x/src/backends/p2p.h +++ b/dep/ggpo-x/src/backends/p2p.h @@ -82,11 +82,11 @@ protected: int nFrame; int checkSum;; }; - std::map _pendingCheckSums; - std::map _confirmedCheckSums; + std::map _pendingCheckSums; + std::map _confirmedCheckSums; // uint16 GetChecksumForConfirmedFrame(int frameNumber) const; - void CheckRemoteChecksum(int framenumber, uint16 cs); + void CheckRemoteChecksum(int framenumber, uint32 cs); int HowFarBackForChecksums()const; int _confirmedCheckSumFrame = -500; void CheckDesync(); diff --git a/dep/ggpo-x/src/backends/synctest.cpp b/dep/ggpo-x/src/backends/synctest.cpp index c82a9c60d..a8fbe0884 100644 --- a/dep/ggpo-x/src/backends/synctest.cpp +++ b/dep/ggpo-x/src/backends/synctest.cpp @@ -158,7 +158,7 @@ SyncTestBackend::IncrementFrame(uint16_t cs) RaiseSyncError("Checksum for frame %d does not match saved (%d != %d)", frame, checksum, info.checksum); } printf("Checksum %08d for frame %d matches.\n", checksum, info.frame); - _callbacks.free_buffer(_callbacks.context, info.buf); + _callbacks.free_buffer(_callbacks.context, info.buf, info.frame); } _last_verified = frame; _rollingback = false; diff --git a/dep/ggpo-x/src/game_input.h b/dep/ggpo-x/src/game_input.h index a19b834ba..834a0d7f3 100644 --- a/dep/ggpo-x/src/game_input.h +++ b/dep/ggpo-x/src/game_input.h @@ -24,7 +24,7 @@ struct GameInput { int frame; int size; /* size in bytes of the entire input for all players */ char bits[GAMEINPUT_MAX_BYTES * GAMEINPUT_MAX_PLAYERS]; - uint16 checksum; + uint32 checksum; bool is_null() { return frame == NullFrame; } void init(int frame, char *bits, int size, int offset); void init(int frame, char *bits, int size); diff --git a/dep/ggpo-x/src/network/udp_msg.h b/dep/ggpo-x/src/network/udp_msg.h index e21d24f2a..1a4b6e4ff 100644 --- a/dep/ggpo-x/src/network/udp_msg.h +++ b/dep/ggpo-x/src/network/udp_msg.h @@ -67,7 +67,7 @@ struct UdpMsg int ack_frame:31; uint16 num_bits; - uint16 checksum16; + uint32 checksum32; uint8 input_size; // XXX: shouldn't be in every single packet! uint8 bits[MAX_COMPRESSED_BITS]; /* must be last */ } input; diff --git a/dep/ggpo-x/src/network/udp_proto.cpp b/dep/ggpo-x/src/network/udp_proto.cpp index bfee20fde..1197ddccb 100644 --- a/dep/ggpo-x/src/network/udp_proto.cpp +++ b/dep/ggpo-x/src/network/udp_proto.cpp @@ -134,7 +134,7 @@ UdpProtocol::SendPendingOutput() ASSERT(last.frame == -1 || last.frame + 1 == msg->u.input.start_frame); for (j = 0; j < _pending_output.size(); j++) { GameInput ¤t = _pending_output.item(j); - msg->u.input.checksum16 = current.checksum; + msg->u.input.checksum32 = current.checksum; if (memcmp(current.bits, last.bits, current.size) != 0) { ASSERT((GAMEINPUT_MAX_BYTES * GAMEINPUT_MAX_PLAYERS * 8) < (1 << BITVECTOR_NIBBLE_SIZE)); for (i = 0; i < current.size * 8; i++) { @@ -627,7 +627,7 @@ UdpProtocol::OnInput(UdpMsg *msg, int len) char desc[1024]; ASSERT(currentFrame == _last_received_input.frame + 1); _last_received_input.frame = currentFrame; - _last_received_input.checksum = msg->u.input.checksum16; + _last_received_input.checksum = msg->u.input.checksum32; /* * Send the event to the emualtor */ diff --git a/dep/ggpo-x/src/network/udp_proto.h b/dep/ggpo-x/src/network/udp_proto.h index 14947e27f..086beda1c 100644 --- a/dep/ggpo-x/src/network/udp_proto.h +++ b/dep/ggpo-x/src/network/udp_proto.h @@ -94,8 +94,8 @@ public: void ApplyToEvents(std::function cb); void StartPollLoop(); void EndPollLoop(); - std::map _remoteCheckSums; - std::map _remoteCheckSumsThisFrame; + std::map _remoteCheckSums; + std::map _remoteCheckSumsThisFrame; protected: enum State { Syncing, diff --git a/dep/ggpo-x/src/sync.cpp b/dep/ggpo-x/src/sync.cpp index 6053d0eb2..a4e69c725 100644 --- a/dep/ggpo-x/src/sync.cpp +++ b/dep/ggpo-x/src/sync.cpp @@ -24,7 +24,7 @@ Sync::~Sync() * structure so we can efficently copy frames via weak references. */ for (int i = 0; i < _savedstate.frames.size(); i++) { - _callbacks.free_buffer(_callbacks.context, _savedstate.frames[i].buf); + _callbacks.free_buffer(_callbacks.context, _savedstate.frames[i].buf, _savedstate.frames[i].frame); } delete [] _input_queues; _input_queues = NULL; @@ -204,7 +204,7 @@ Sync::SaveCurrentFrame() */ SavedFrame *state = &_savedstate.frames[_savedstate.head]; if (state->buf) { - _callbacks.free_buffer(_callbacks.context, state->buf); + _callbacks.free_buffer(_callbacks.context, state->buf, state->frame); state->buf = NULL; } state->frame = _framecount; diff --git a/dep/ggpo-x/src/sync.h b/dep/ggpo-x/src/sync.h index 76308ae2b..64e8353fd 100644 --- a/dep/ggpo-x/src/sync.h +++ b/dep/ggpo-x/src/sync.h @@ -61,7 +61,8 @@ public: bool GetEvent(Event& e); int MaxPredictionFrames() const { return _max_prediction_frames; } protected: - friend SyncTestBackend; + friend class SyncTestBackend; + friend class Peer2PeerBackend; struct SavedFrame { byte* buf; diff --git a/src/core/netplay.cpp b/src/core/netplay.cpp index 8630b1ddc..48577c10b 100644 --- a/src/core/netplay.cpp +++ b/src/core/netplay.cpp @@ -7,12 +7,14 @@ #include "common/timer.h" #include "digital_controller.h" #include "ggponet.h" +#include "host.h" #include "host_settings.h" #include "pad.h" #include "spu.h" #include "system.h" #include #include +#include Log_SetChannel(Netplay); #ifdef _WIN32 @@ -32,7 +34,7 @@ static bool NpAdvFrameCb(void* ctx, int flags); static bool NpSaveFrameCb(void* ctx, unsigned char** buffer, int* len, int* checksum, int frame); static bool NpLoadFrameCb(void* ctx, unsigned char* buffer, int len, int rb_frames, int frame_to_load); static bool NpBeginGameCb(void* ctx, const char* game_name); -static void NpFreeBuffCb(void* ctx, void* buffer); +static void NpFreeBuffCb(void* ctx, void* buffer, int frame); static bool NpOnEventCb(void* ctx, GGPOEvent* ev); static Input ReadLocalInput(); @@ -45,7 +47,7 @@ static void SetSettings(); // l = local, r = remote static s32 Start(s32 lhandle, u16 lport, std::string& raddr, u16 rport, s32 ldelay, u32 pred); -static void AdvanceFrame(u16 checksum = 0); +static void AdvanceFrame(); static void RunFrame(); static s32 CurrentFrame(); @@ -57,6 +59,9 @@ static void InitializeFramePacing(); static void HandleTimeSyncEvent(float frame_delta, int update_interval); static void Throttle(); +// Desync Detection +static void GenerateChecksumForFrame(int* checksum, int frame, unsigned char* buffer, int buffer_size); +static void GenerateDesyncReport(s32 desync_frame); ////////////////////////////////////////////////////////////////////////// // Variables ////////////////////////////////////////////////////////////////////////// @@ -107,7 +112,7 @@ s32 Netplay::Start(s32 lhandle, u16 lport, std::string& raddr, u16 rport, s32 ld ggpo_start_session(&s_ggpo, &cb, "Duckstation-Netplay", 2, sizeof(Netplay::Input), lport, MAX_ROLLBACK_FRAMES); // result = ggpo_start_synctest(&s_ggpo, &cb, (char*)"asdf", 2, sizeof(Netplay::Input), 1); - ggpo_set_disconnect_timeout(s_ggpo, 3000); + ggpo_set_disconnect_timeout(s_ggpo, 2000); ggpo_set_disconnect_notify_start(s_ggpo, 1000); for (int i = 1; i <= 2; i++) @@ -176,6 +181,11 @@ void Netplay::SetSettings() 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 + si.SetBoolValue("GPU", "UseSoftwareRendererForReadbacks", true); + Host::Internal::SetNetplaySettingsLayer(&si); } @@ -200,19 +210,16 @@ void Netplay::UpdateThrottlePeriod() void Netplay::HandleTimeSyncEvent(float frame_delta, int update_interval) { - // threshold to what is is with correcting for. - if (frame_delta <= 1.0f) - return; - - float total_time = frame_delta * s_frame_period; - // Distribute the frame difference over the next N * 0.8 frames. + // Distribute the frame difference over the next N * 0.75 frames. // only part of the interval time is used since we want to come back to normal speed. // otherwise we will keep spiraling into unplayable gameplay. - float added_time_per_frame = -(total_time / (static_cast(update_interval) * 0.8f)); + float total_time = (frame_delta * s_frame_period) / 4; + float mun_timesync_frames = update_interval * 0.75f; + float added_time_per_frame = -(total_time / mun_timesync_frames); float iterations_per_frame = 1.0f / s_frame_period; s_target_speed = (s_frame_period + added_time_per_frame) * iterations_per_frame; - s_next_timesync_recovery_frame = CurrentFrame() + static_cast(std::ceil(static_cast(update_interval) * 0.8f)); + s_next_timesync_recovery_frame = CurrentFrame() + static_cast(std::ceil(mun_timesync_frames)); UpdateThrottlePeriod(); @@ -264,15 +271,57 @@ void Netplay::Throttle() } } -void Netplay::AdvanceFrame(u16 checksum) +void Netplay::GenerateChecksumForFrame(int* checksum, int frame, unsigned char* buffer, int buffer_size) { - ggpo_advance_frame(s_ggpo, checksum); + const u32 sliding_window_size = 4096 * 4; // 4 pages. + const u32 num_group_of_pages = buffer_size / sliding_window_size; + const u32 start_position = (frame % num_group_of_pages) * sliding_window_size; + *checksum = XXH32(buffer + start_position, sliding_window_size, frame); + // Log_VerbosePrintf("Netplay Checksum: f:%d wf:%d c:%u", frame, frame % num_group_of_pages, *checksum); +} + +void Netplay::GenerateDesyncReport(s32 desync_frame) +{ + std::string path = "\\netplaylogs\\desync_frame_" + std::to_string(desync_frame) + "_p" + + std::to_string(s_local_handle) + "_" + System::GetRunningSerial() + "_.txt"; + std::string filename = EmuFolders::Dumps + path; + + std::unique_ptr stream = + ByteStream::OpenFile(filename.c_str(), BYTESTREAM_OPEN_CREATE | BYTESTREAM_OPEN_WRITE | BYTESTREAM_OPEN_TRUNCATE | + BYTESTREAM_OPEN_ATOMIC_UPDATE | BYTESTREAM_OPEN_STREAMED); + if (!stream) + { + Log_VerbosePrint("desync log creation failed to create stream"); + return; + } + + if (!ByteStream::WriteBinaryToStream(stream.get(), + s_save_buffer_pool.back().get()->state_stream.get()->GetMemoryPointer(), + s_save_buffer_pool.back().get()->state_stream.get()->GetMemorySize())) + { + Log_VerbosePrint("desync log creation failed to write the stream"); + stream->Discard(); + return; + } + /* stream->Write(s_save_buffer_pool.back().get()->state_stream.get()->GetMemoryPointer(), + s_save_buffer_pool.back().get()->state_stream.get()->GetMemorySize());*/ + + stream->Commit(); + + Log_VerbosePrintf("desync log created for frame %d", desync_frame); +} + + +void Netplay::AdvanceFrame() +{ + ggpo_advance_frame(s_ggpo, 0); } void Netplay::RunFrame() { + // housekeeping + ggpo_idle(s_ggpo); // run game - bool need_idle = true; auto result = GGPO_OK; int disconnect_flags = 0; Netplay::Input inputs[2] = {}; @@ -291,13 +340,8 @@ void Netplay::RunFrame() // enable again when rolling back done SPU::SetAudioOutputMuted(false); NetplayAdvanceFrame(inputs, disconnect_flags); - // coming here means that the system doesnt need to idle anymore - need_idle = false; } } - // allow ggpo to do housekeeping if needed - if (need_idle) - ggpo_idle(s_ggpo); } s32 Netplay::CurrentFrame() @@ -372,9 +416,14 @@ void Netplay::StartNetplaySession(s32 local_handle, u16 local_port, std::string& s_game_path = std::move(game_path); // create session int result = Netplay::Start(local_handle, local_port, remote_addr, remote_port, input_delay, MAX_ROLLBACK_FRAMES); + // notify that the session failed if (result != GGPO_OK) - { Log_ErrorPrintf("Failed to Create Netplay Session! Error: %d", result); + else + { + // Load savestate if available + std::string save = EmuFolders::SaveStates + "/netplay/" + System::GetRunningSerial() + ".sav"; + System::LoadState(save.c_str()); } } @@ -425,11 +474,12 @@ bool Netplay::NpBeginGameCb(void* ctx, const char* game_name) StopNetplaySession(); return false; } - // Fast Forward to Game Start SPU::SetAudioOutputMuted(true); + // Fast Forward to Game Start if needed. while (System::GetInternalFrameNumber() < 2) System::RunFrame(); SPU::SetAudioOutputMuted(false); + // Set Initial Frame Pacing InitializeFramePacing(); return true; } @@ -446,24 +496,31 @@ bool Netplay::NpAdvFrameCb(void* ctx, int flags) bool Netplay::NpSaveFrameCb(void* ctx, unsigned char** buffer, int* len, int* checksum, int frame) { SaveStateBuffer our_buffer; - if (s_save_buffer_pool.empty()) + // min size is 2 because otherwise the desync logger doesnt have enough time to dump the state. + if (s_save_buffer_pool.size() < 2) { our_buffer = std::make_unique(); } else { - our_buffer = std::move(s_save_buffer_pool.back()); - s_save_buffer_pool.pop_back(); + our_buffer = std::move(s_save_buffer_pool.front()); + s_save_buffer_pool.pop_front(); } if (!System::SaveMemoryState(our_buffer.get())) { - s_save_buffer_pool.push_back(std::move(our_buffer)); + s_save_buffer_pool.push_front(std::move(our_buffer)); return false; } + // desync detection + const u32 state_size = our_buffer.get()->state_stream.get()->GetMemorySize(); + unsigned char* state = reinterpret_cast(our_buffer.get()->state_stream.get()->GetMemoryPointer()); + GenerateChecksumForFrame(checksum, frame, state, state_size); + *len = sizeof(System::MemorySaveState); *buffer = reinterpret_cast(our_buffer.release()); + return true; } @@ -475,8 +532,9 @@ bool Netplay::NpLoadFrameCb(void* ctx, unsigned char* buffer, int len, int rb_fr return System::LoadMemoryState(*reinterpret_cast(buffer)); } -void Netplay::NpFreeBuffCb(void* ctx, void* buffer) +void Netplay::NpFreeBuffCb(void* ctx, void* buffer, int frame) { + // Log_VerbosePrintf("Reuse Buffer: %d", frame); SaveStateBuffer our_buffer(reinterpret_cast(buffer)); s_save_buffer_pool.push_back(std::move(our_buffer)); } @@ -484,7 +542,7 @@ void Netplay::NpFreeBuffCb(void* ctx, void* buffer) bool Netplay::NpOnEventCb(void* ctx, GGPOEvent* ev) { char buff[128]; - std::string msg; + std::string msg, filename; switch (ev->code) { case GGPOEventCode::GGPO_EVENTCODE_CONNECTED_TO_PEER: @@ -523,10 +581,14 @@ bool Netplay::NpOnEventCb(void* ctx, GGPOEvent* ev) HandleTimeSyncEvent(ev->u.timesync.frames_ahead, ev->u.timesync.timeSyncPeriodInFrames); 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); + sprintf(buff, "Desync Detected: Current Frame: %d, Desync Frame: %d, Diff: %d, L:%u, R:%u", CurrentFrame(), + ev->u.desync.nFrameOfDesync, CurrentFrame() - ev->u.desync.nFrameOfDesync, ev->u.desync.ourCheckSum, + ev->u.desync.remoteChecksum); msg = buff; - break; + GenerateDesyncReport(ev->u.desync.nFrameOfDesync); + Host::AddKeyedOSDMessage("Netplay", msg, 5); + + return true; default: sprintf(buff, "Netplay Event Code: %d", ev->code); msg = buff; diff --git a/src/core/system.cpp b/src/core/system.cpp index 606db61d4..3704ba65f 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -1752,6 +1752,23 @@ bool System::DoState(StateWrapper& sw, GPUTexture** host_texture, bool update_di cpu_overclock_active ? 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(); } diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index e9e906b28..2520a6b53 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -1466,7 +1466,7 @@ void EmuThread::run() // main loop while (!m_shutdown_flag) { - if (Netplay::IsActive()) + if (Netplay::IsActive() && System::IsRunning()) { Netplay::ExecuteNetplay(); }