From d5723669de680047fcf3814ba375c8480dd247e6 Mon Sep 17 00:00:00 2001 From: Nikhil Narayana Date: Mon, 30 May 2022 09:26:12 -0700 Subject: [PATCH] pull in project-slippi/Ishiiruka/commit/923195dd5f662a5e65f85b10162fe14b1f0cd57a --- Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp | 151 +++++++++++++------ Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h | 6 +- Source/Core/Core/Slippi/SlippiNetplay.cpp | 102 +++++++++---- Source/Core/Core/Slippi/SlippiNetplay.h | 7 +- 4 files changed, 186 insertions(+), 80 deletions(-) diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp index ba5156eae2..7f29ec43f5 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp +++ b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp @@ -1532,7 +1532,8 @@ void CEXISlippi::handleOnlineInputs(u8* payload) { m_read_queue.clear(); - int32_t frame = payload[0] << 24 | payload[1] << 16 | payload[2] << 8 | payload[3]; + s32 frame = Common::swap32(&payload[0]); + s32 finalizedFrame = Common::swap32(&payload[4]); if (frame == 1) { @@ -1571,19 +1572,25 @@ void CEXISlippi::handleOnlineInputs(u8* payload) return; } - if (shouldSkipOnlineFrame(frame)) + // Drop inputs that we no longer need (inputs older than the finalized frame passed in) + slippi_netplay->DropOldRemoteInputs(finalizedFrame); + + bool shouldSkip = shouldSkipOnlineFrame(frame, finalizedFrame); + if (shouldSkip) { // Send inputs that have not yet been acked slippi_netplay->SendSlippiPad(nullptr); - m_read_queue.push_back(2); - return; + } + else + { + // Send the input for this frame along with everything that has yet to be acked + handleSendInputs(payload); } - handleSendInputs(payload); - prepareOpponentInputs(payload); + prepareOpponentInputs(frame, shouldSkip); } -bool CEXISlippi::shouldSkipOnlineFrame(s32 frame) +bool CEXISlippi::shouldSkipOnlineFrame(s32 frame, s32 finalizedFrame) { auto status = slippi_netplay->GetSlippiConnectStatus(); bool connectionFailed = @@ -1603,8 +1610,16 @@ bool CEXISlippi::shouldSkipOnlineFrame(s32 frame) // Return true if we are too far ahead for rollback. ROLLBACK_MAX_FRAMES is the number of frames // we can receive for the opponent at one time and is our "look-ahead" limit - int32_t latestRemoteFrame = slippi_netplay->GetSlippiLatestRemoteFrame(); - if (frame - latestRemoteFrame >= ROLLBACK_MAX_FRAMES) + // Example: finalizedFrame = 100 means the last savestate we need is 101. We can then store + // states 101 to 107 before running out of savestates. So 107 - 100 = 7. We need to make sure + // we have enough inputs to finalize to not overflow the available states, so if our latest frame + // is 101, we can't let frame 109 be created. 101 - 100 >= 109 - 100 - 7 : 1 >= 2 (false). + // It has to work this way because we only have room to move our states forward by one for frame + // 108 + s32 latestRemoteFrame = slippi_netplay->GetSlippiLatestRemoteFrame(ROLLBACK_MAX_FRAMES); + auto hasEnoughNewInputs = + latestRemoteFrame - finalizedFrame >= (frame - finalizedFrame - ROLLBACK_MAX_FRAMES); + if (!hasEnoughNewInputs) { stallFrameCount++; if (stallFrameCount > 60 * 7) @@ -1613,16 +1628,19 @@ bool CEXISlippi::shouldSkipOnlineFrame(s32 frame) isConnectionStalled = true; } - WARN_LOG(SLIPPI_ONLINE, - "Halting for one frame due to rollback limit (frame: %d | latest: %d)...", frame, - latestRemoteFrame); + WARN_LOG_FMT( + SLIPPI_ONLINE, + "Halting for one frame due to rollback limit (frame: {} | latest: {} | finalized: {})...", + frame, latestRemoteFrame, finalizedFrame); return true; } stallFrameCount = 0; - // Return true if we are over 60% of a frame ahead of our opponent. Currently limiting how - // often this happens because I'm worried about jittery data causing a lot of unneccesary delays. + s32 frameTime = 16683; + s32 t1 = 10000; + s32 t2 = (2 * frameTime) + t1; + // Only skip once for a given frame because our time detection method doesn't take into // consideration waiting for a frame. Also it's less jarring and it happens often enough that it // will smoothly get to the right place @@ -1632,10 +1650,14 @@ bool CEXISlippi::shouldSkipOnlineFrame(s32 frame) auto offsetUs = slippi_netplay->CalcTimeOffsetUs(); INFO_LOG_FMT(SLIPPI_ONLINE, "[Frame {}] Offset for skip is: {} us", frame, offsetUs); + // At the start of the game, let's make sure to sync perfectly, but after that let the slow + // instance try to do more work before we stall + // The decision to skip a frame only happens when we are already pretty far off ahead. The hope - // is that this won't really be used much because the frame advance of the slow client will pick - // up the difference most of the time. But at some point it's probably better to slow down... - if (offsetUs > 26000) + // is that this won't really be used much because the frame advance of the slow client along + // with dynamic emulation speed will pick up the difference most of the time. But at some point + // it's probably better to slow down... + if (offsetUs > (frame <= 120 ? t1 : t2)) { isCurrentlySkipping = true; @@ -1665,6 +1687,17 @@ bool CEXISlippi::shouldSkipOnlineFrame(s32 frame) bool CEXISlippi::shouldAdvanceOnlineFrame(s32 frame) { + // Logic below is used to test frame advance by forcing it more often + // SConfig::GetInstance().m_EmulationSpeed = 0.5f; + // if (frame > 120 && frame % 10 < 3) + //{ + // Common::SleepCurrentThread(1); // Sleep to try to let inputs come in to make late rollbacks + // more likely return true; + //} + + // return false; + // return frame % 2 == 0; + // Return true if we are over 60% of a frame behind our opponent. We limit how often this happens // to get a reliable average to act on. We will allow advancing up to 5 frames (spread out) over // the 30 frame period. This makes the game feel relatively smooth still @@ -1676,12 +1709,30 @@ bool CEXISlippi::shouldAdvanceOnlineFrame(s32 frame) // Dynamically adjust emulation speed in order to fine-tune time sync to reduce one sided // rollbacks even more - auto maxSpeedDeviation = 0.005f; - auto deviation = (offsetUs / 33366.0f) * maxSpeedDeviation; - if (deviation > maxSpeedDeviation) - deviation = maxSpeedDeviation; - else if (deviation < -maxSpeedDeviation) - deviation = -maxSpeedDeviation; + // Modify emulation speed up to a max of 1% at 3 frames offset or more. Don't slow down the + // front instance as much because we want to prioritize performance for the fast PC + float deviation = 0; + float maxSlowDownAmount = 0.005f; + float maxSpeedUpAmount = 0.01f; + int frameWindow = 3; + if (offsetUs > -250 && offsetUs < 8000) + { + // Do nothing, leave deviation at 0 for 100% emulation speed when ahead by 8 ms or less + } + else if (offsetUs < 0) + { + // Here we are behind, so let's speed up our instance + float frameWindowMultiplier = std::min(-offsetUs / (frameWindow * 16683.0f), 1.0f); + deviation = frameWindowMultiplier * maxSpeedUpAmount; + } + else + { + // Here we are ahead, so let's slow down our instance + float frameWindowMultiplier = std::min(offsetUs / (frameWindow * 16683.0f), 1.0f); + deviation = frameWindowMultiplier * -maxSlowDownAmount; + } + + auto dynamicEmulationSpeed = 1.0f + deviation; // If we are behind (negative offset) we want to go above 100% run speed, so we need to subtract // the deviation value @@ -1692,13 +1743,17 @@ bool CEXISlippi::shouldAdvanceOnlineFrame(s32 frame) INFO_LOG(SLIPPI_ONLINE, "[Frame %d] Offset for advance is: %d us. New speed: %.2f%%", frame, offsetUs, dynamicEmulationSpeed * 100.0f); + s32 frameTime = 16683; + s32 t1 = 10000; + s32 t2 = frameTime + t1; + // Count the number of times we're below a threshold we should easily be able to clear. This is // checked twice per second. - fallBehindCounter += offsetUs < -10000 ? 1 : 0; - fallFarBehindCounter += offsetUs < -25000 ? 1 : 0; + fallBehindCounter += offsetUs < -t1 ? 1 : 0; + fallFarBehindCounter += offsetUs < -t2 ? 1 : 0; - bool isSlow = (offsetUs < -10000 && fallBehindCounter > 50) || - (offsetUs < -25000 && fallFarBehindCounter > 15); + bool isSlow = + (offsetUs < -t1 && fallBehindCounter > 50) || (offsetUs < -t2 && fallFarBehindCounter > 15); if (isSlow && lastSearch.mode != SlippiMatchmaking::OnlinePlayMode::TEAMS) { // We don't show this message for teams because it seems to false positive a lot there, maybe @@ -1710,13 +1765,13 @@ bool CEXISlippi::shouldAdvanceOnlineFrame(s32 frame) OSD::Color::RED); } - if (offsetUs < -10000 && !isCurrentlyAdvancing) + if (offsetUs < -t2 && !isCurrentlyAdvancing) { isCurrentlyAdvancing = true; // On early frames, don't advance any frames. Let the stalling logic handle the initial sync - int maxAdvFrames = frame > 120 ? 5 : 0; - framesToAdvance = ((-offsetUs - 10000) / 16683) + 1; + int maxAdvFrames = frame > 120 ? 3 : 0; + framesToAdvance = ((-offsetUs - t1) / frameTime) + 1; framesToAdvance = framesToAdvance > maxAdvFrames ? maxAdvFrames : framesToAdvance; WARN_LOG(SLIPPI_ONLINE, @@ -1747,8 +1802,8 @@ void CEXISlippi::handleSendInputs(u8* payload) if (isConnectionStalled) return; - int32_t frame = payload[0] << 24 | payload[1] << 16 | payload[2] << 8 | payload[3]; - u8 delay = payload[4]; + s32 frame = Common::swap32(&payload[0]); + u8 delay = payload[8]; // On the first frame sent, we need to queue up empty dummy pads for as many // frames as we have delay @@ -1761,22 +1816,27 @@ void CEXISlippi::handleSendInputs(u8* payload) } } - auto pad = std::make_unique(frame + delay, &payload[5]); + auto pad = std::make_unique(frame + delay, &payload[9]); slippi_netplay->SendSlippiPad(std::move(pad)); } -void CEXISlippi::prepareOpponentInputs(u8* payload) +void CEXISlippi::prepareOpponentInputs(s32 frame, bool shouldSkip) { m_read_queue.clear(); - int32_t frame = payload[0] << 24 | payload[1] << 16 | payload[2] << 8 | payload[3]; - u8 frameResult = 1; // Indicates to continue frame auto state = slippi_netplay->GetSlippiConnectStatus(); - if (state != SlippiNetplayClient::SlippiConnectStatus::NET_CONNECT_STATUS_CONNECTED || - isConnectionStalled) + if (shouldSkip) + { + // Event though we are skipping an input, we still want to prepare the opponent inputs because + // in the case where we get a stall on an advance frame, we need to keep the RXB inputs + // populated for when the frame inputs are requested on a rollback + frameResult = 2; + } + else if (state != SlippiNetplayClient::SlippiConnectStatus::NET_CONNECT_STATUS_CONNECTED || + isConnectionStalled) { frameResult = 3; // Indicates we have disconnected } @@ -1785,19 +1845,21 @@ void CEXISlippi::prepareOpponentInputs(u8* payload) frameResult = 4; } - m_read_queue.push_back(frameResult); // Indicate a continue frame + m_read_queue.push_back(frameResult); // Write out the control message value u8 remotePlayerCount = matchmaking->RemotePlayerCount(); m_read_queue.push_back(remotePlayerCount); // Indicate the number of remote players std::unique_ptr results[SLIPPI_REMOTE_PLAYER_MAX]; int offset[SLIPPI_REMOTE_PLAYER_MAX]; - INFO_LOG(SLIPPI_ONLINE, "Preparing pad data for frame %d", frame); + + int32_t latestFrameRead[SLIPPI_REMOTE_PLAYER_MAX]{}; // Get pad data for each remote player and write each of their latest frame nums to the buf for (int i = 0; i < remotePlayerCount; i++) { - results[i] = slippi_netplay->GetSlippiRemotePad(frame, i); + results[i] = slippi_netplay->GetSlippiRemotePad(i, ROLLBACK_MAX_FRAMES); + // results[i] = slippi_netplay->GetFakePadOutput(frame); // determine offset from which to copy data offset[i] = (results[i]->latestFrame - frame) * SLIPPI_PAD_FULL_SIZE; @@ -1807,16 +1869,19 @@ void CEXISlippi::prepareOpponentInputs(u8* payload) int32_t latestFrame = results[i]->latestFrame; if (latestFrame > frame) latestFrame = frame; - appendWordToBuffer(&m_read_queue, *(u32*)&latestFrame); + appendWordToBuffer(&m_read_queue, static_cast(latestFrame)); // INFO_LOG(SLIPPI_ONLINE, "Sending frame num %d for pIdx %d (offset: %d)", latestFrame, i, // offset[i]); } // Send the current frame for any unused player slots. for (int i = remotePlayerCount; i < SLIPPI_REMOTE_PLAYER_MAX; i++) { - appendWordToBuffer(&m_read_queue, *(u32*)&frame); + appendWordToBuffer(&m_read_queue, static_cast(frame)); } + s32* val = std::min_element(std::begin(latestFrameRead), std::end(latestFrameRead)); + appendWordToBuffer(&m_read_queue, static_cast(*val)); + // copy pad data over for (int i = 0; i < SLIPPI_REMOTE_PLAYER_MAX; i++) { diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h index d6e17c1d26..b9660c06b6 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h +++ b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h @@ -112,7 +112,7 @@ private: {CMD_GET_GECKO_CODES, 0}, // The following are used for Slippi online and also have fixed sizes - {CMD_ONLINE_INPUTS, 17}, + {CMD_ONLINE_INPUTS, 21}, {CMD_CAPTURE_SAVESTATE, 32}, {CMD_LOAD_SAVESTATE, 32}, {CMD_GET_MATCH_STATE, 0}, @@ -173,7 +173,7 @@ private: u16 getRandomStage(); bool isDisconnected(); void handleOnlineInputs(u8* payload); - void prepareOpponentInputs(u8* payload); + void prepareOpponentInputs(s32 frame, bool shouldSkip); void handleSendInputs(u8* payload); void handleCaptureSavestate(u8* payload); void handleLoadSavestate(u8* payload); @@ -181,7 +181,7 @@ private: void startFindMatch(u8* payload); void prepareOnlineMatchState(); void setMatchSelections(u8* payload); - bool shouldSkipOnlineFrame(s32 frame); + bool shouldSkipOnlineFrame(s32 frame, s32 finalizedFrame); bool shouldAdvanceOnlineFrame(s32 frame); void handleLogInRequest(); void handleLogOutRequest(); diff --git a/Source/Core/Core/Slippi/SlippiNetplay.cpp b/Source/Core/Core/Slippi/SlippiNetplay.cpp index 6debbfdcc1..3f6de4d7e1 100644 --- a/Source/Core/Core/Slippi/SlippiNetplay.cpp +++ b/Source/Core/Core/Slippi/SlippiNetplay.cpp @@ -289,8 +289,8 @@ unsigned int SlippiNetplayClient::OnData(sf::Packet& packet, ENetPeer* peer) int32_t headFrame = remotePadQueue[pIdx].empty() ? 0 : remotePadQueue[pIdx].front()->frame; int inputsToCopy = frame - headFrame; - // Check that the packet actually contains the data it claims to - if ((5 + inputsToCopy * SLIPPI_PAD_DATA_SIZE) > (int)packet.getDataSize()) + // Not sure what the max is here. If we never ack frames it could get big... + if (inputsToCopy > 128) { ERROR_LOG_FMT( SLIPPI_ONLINE, @@ -723,9 +723,9 @@ void SlippiNetplayClient::ThreadFunc() if (isAlreadyConnected) { - // Don't add this person again if they are already connected. Not doing this can cause one - // person to take up 2 or more spots, denying one or more players from connecting and thus - // getting stuck on the "Waiting" step + // Don't add this person again if they are already connected. Not doing this can cause + // one person to take up 2 or more spots, denying one or more players from connecting + // and thus getting stuck on the "Waiting" step INFO_LOG(SLIPPI_ONLINE, "Already connected!"); break; // Breaks out of case } @@ -735,12 +735,12 @@ void SlippiNetplayClient::ThreadFunc() // This check used to check for port as well as host. The problem was that for some // people, their internet will switch the port they're sending from. This means these // people struggle to connect to others but they sometimes do succeed. When we were - // checking for port here though we would get into a state where the person they succeeded - // to connect to would not accept the connection with them, this would lead the player - // with this internet issue to get stuck waiting for the other player. The only downside - // to this that I can guess is that if you fail to connect to one person out of two that - // are on your LAN, it might report that you failed to connect to the wrong person. There - // might be more problems tho, not sure + // checking for port here though we would get into a state where the person they + // succeeded to connect to would not accept the connection with them, this would lead + // the player with this internet issue to get stuck waiting for the other player. The + // only downside to this that I can guess is that if you fail to connect to one person + // out of two that are on your LAN, it might report that you failed to connect to the + // wrong person. There might be more problems tho, not sure INFO_LOG_FMT(SLIPPI_ONLINE, "[Netplay] Comparing connection address: {} - {}", remoteAddrs[i].host, netEvent.peer->address.host); if (remoteAddrs[i].host == netEvent.peer->address.host && !connections[i]) @@ -1106,8 +1106,39 @@ u8 SlippiNetplayClient::GetSlippiRemoteSentChatMessage() return copiedMessageId; } -std::unique_ptr SlippiNetplayClient::GetSlippiRemotePad(int32_t curFrame, - int index) +std::unique_ptr SlippiNetplayClient::GetFakePadOutput(int frame) +{ + // Used for testing purposes, will ignore the opponent's actual inputs and provide fake + // ones to trigger rollback scenarios + std::unique_ptr padOutput = std::make_unique(); + + // Triggers rollback where the first few inputs were correctly predicted + if (frame % 60 < 5) + { + // Return old inputs for a bit + padOutput->latestFrame = frame - (frame % 60); + padOutput->data.insert(padOutput->data.begin(), SLIPPI_PAD_FULL_SIZE, 0); + } + else if (frame % 60 == 5) + { + padOutput->latestFrame = frame; + // Add 5 frames of 0'd inputs + padOutput->data.insert(padOutput->data.begin(), 5 * SLIPPI_PAD_FULL_SIZE, 0); + + // Press A button for 2 inputs prior to this frame causing a rollback + padOutput->data[2 * SLIPPI_PAD_FULL_SIZE] = 1; + } + else + { + padOutput->latestFrame = frame; + padOutput->data.insert(padOutput->data.begin(), SLIPPI_PAD_FULL_SIZE, 0); + } + + return std::move(padOutput); +} + +std::unique_ptr SlippiNetplayClient::GetSlippiRemotePad(int index, + int maxFrameCount) { std::lock_guard lk(pad_mutex); // TODO: Is this the correct lock? @@ -1125,21 +1156,37 @@ std::unique_ptr SlippiNetplayClient::GetSlippiRemotePad(i return std::move(padOutput); } + int inputCount = 0; + padOutput->latestFrame = 0; - // Copy the entire remaining remote buffer - for (auto it = remotePadQueue[index].begin(); it != remotePadQueue[index].end(); ++it) + + // Copy inputs from the remote pad queue to the output. We iterate backwards because + // we want to get the oldest frames possible (will have been cleared to contain the last + // finalized frame at the back). I think it's very unlikely but I think before we + // iterated from the front and it's possible the 7 frame limit left out an input the + // game actually needed. + for (auto it = remotePadQueue[index].rbegin(); it != remotePadQueue[index].rend(); ++it) { if ((*it)->frame > padOutput->latestFrame) padOutput->latestFrame = (*it)->frame; + // NOTICE_LOG(SLIPPI_ONLINE, "[%d] (Remote) P%d %08X %08X %08X", (*it)->frame, + // index >= playerIdx ? index + 1 : index, Common::swap32(&(*it)->padBuf[0]), + // Common::swap32(&(*it)->padBuf[4]), Common::swap32(&(*it)->padBuf[8])); + auto padIt = std::begin((*it)->padBuf); - padOutput->data.insert(padOutput->data.end(), padIt, padIt + SLIPPI_PAD_FULL_SIZE); + padOutput->data.insert(padOutput->data.begin(), padIt, padIt + SLIPPI_PAD_FULL_SIZE); + + // Limit max amount of inputs to send + inputCount++; + if (inputCount >= maxFrameCount) + break; } return std::move(padOutput); } -void SlippiNetplayClient::DropOldRemoteInputs(int32_t curFrame) +void SlippiNetplayClient::DropOldRemoteInputs(int32_t finalizedFrame) { std::lock_guard lk(pad_mutex); @@ -1162,13 +1209,10 @@ void SlippiNetplayClient::DropOldRemoteInputs(int32_t curFrame) for (int i = 0; i < m_remotePlayerCount; i++) { // INFO_LOG_FMT(SLIPPI_ONLINE, "remotePadQueue[{}] size: {}", i, remotePadQueue[i].size()); - while (remotePadQueue[i].size() > 1 && remotePadQueue[i].back()->frame < lowestCommonFrame && - remotePadQueue[i].back()->frame < curFrame) - { + while (remotePadQueue[i].size() > 1 && remotePadQueue[i].back()->frame < finalizedFrame) /*INFO_LOG_FMT(SLIPPI_ONLINE, "Popping inputs for frame {} from back of player {} queue", remotePadQueue[i].back()->frame, i);*/ remotePadQueue[i].pop_back(); - } } } @@ -1177,23 +1221,19 @@ SlippiMatchInfo* SlippiNetplayClient::GetMatchInfo() return &matchInfo; } -int32_t SlippiNetplayClient::GetSlippiLatestRemoteFrame() +int32_t SlippiNetplayClient::GetSlippiLatestRemoteFrame(int maxFrameCount) { - std::lock_guard lk(pad_mutex); // TODO: Is this the correct lock? - // Return the lowest frame among remote queues int lowestFrame = 0; + bool isFrameSet = false; for (int i = 0; i < m_remotePlayerCount; i++) { - if (remotePadQueue[i].empty()) - { - return 0; - } - - int f = remotePadQueue[i].front()->frame; - if (f < lowestFrame || lowestFrame == 0) + auto rp = GetSlippiRemotePad(i, maxFrameCount); + int f = rp->latestFrame; + if (f < lowestFrame || !isFrameSet) { lowestFrame = f; + isFrameSet = true; } } diff --git a/Source/Core/Core/Slippi/SlippiNetplay.h b/Source/Core/Core/Slippi/SlippiNetplay.h index 445b649bb5..482b4e971f 100644 --- a/Source/Core/Core/Slippi/SlippiNetplay.h +++ b/Source/Core/Core/Slippi/SlippiNetplay.h @@ -137,10 +137,11 @@ public: void SendConnectionSelected(); void SendSlippiPad(std::unique_ptr pad); void SetMatchSelections(SlippiPlayerSelections& s); - std::unique_ptr GetSlippiRemotePad(int32_t curFrame, int index); - void DropOldRemoteInputs(int32_t curFrame); + std::unique_ptr GetFakePadOutput(int frame); + std::unique_ptr GetSlippiRemotePad(int index, int maxFrameCount); + void DropOldRemoteInputs(int32_t finalizedFrame); SlippiMatchInfo* GetMatchInfo(); - int32_t GetSlippiLatestRemoteFrame(); + int32_t GetSlippiLatestRemoteFrame(int maxFrameCount); SlippiPlayerSelections GetSlippiRemoteChatMessage(); u8 GetSlippiRemoteSentChatMessage(); s32 CalcTimeOffsetUs();