From e3440a2a06027252fad064fc3dc5ae92a8f1977c Mon Sep 17 00:00:00 2001 From: genxium Date: Tue, 31 Jan 2023 22:39:21 +0800 Subject: [PATCH] Fixes for UDP use in input prediction. --- README.md | 2 +- battle_srv/models/player.go | 9 ++-- battle_srv/models/room.go | 21 +++++++-- frontend/assets/scripts/Map.js | 60 ++++++++++++++++--------- frontend/assets/scripts/WsSessionMgr.js | 2 +- 5 files changed, 65 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index c4eec99..f3dff2a 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ _(how input delay roughly works)_ ![input_delay_intro](./charts/InputDelayIntro.jpg) -_(how rollback-and-chase in this project roughly works, kindly note that by the current implementation, each frontend only maintains a `lastAllConfirmedInputFrameId` for all the other peers, because the backend only downsyncs all-confirmed inputFrames, see [markConfirmationIfApplicable](https://github.com/genxium/DelayNoMore/blob/v0.9.14/battle_srv/models/room.go#L1085) for more information -- if a serverless peer-to-peer communication is seriously needed here, consider porting [markConfirmationIfApplicable](https://github.com/genxium/DelayNoMore/blob/v0.9.14/battle_srv/models/room.go#L1085) into frontend for maintaining `lastAllConfirmedInputFrameId` under chaotic reception order of inputFrames from peers)_ +_(how rollback-and-chase in this project roughly works)_ ![server_clients](./charts/ServerClients.jpg) ![rollback_and_chase_intro](./charts/RollbackAndChase.jpg) diff --git a/battle_srv/models/player.go b/battle_srv/models/player.go index eb11448..c4bcd03 100644 --- a/battle_srv/models/player.go +++ b/battle_srv/models/player.go @@ -47,10 +47,11 @@ type Player struct { TutorialStage int `db:"tutorial_stage"` // other in-battle info fields - LastReceivedInputFrameId int32 - LastSentInputFrameId int32 - AckingFrameId int32 - AckingInputFrameId int32 + LastReceivedInputFrameId int32 + LastUdpReceivedInputFrameId int32 + LastSentInputFrameId int32 + AckingFrameId int32 + AckingInputFrameId int32 UdpAddr *PeerUdpAddr BattleUdpTunnelAddr *net.UDPAddr // This addr is used by backend only, not visible to frontend diff --git a/battle_srv/models/room.go b/battle_srv/models/room.go index 978b592..9fcff3e 100644 --- a/battle_srv/models/room.go +++ b/battle_srv/models/room.go @@ -136,7 +136,7 @@ type Room struct { EffectivePlayerCount int32 DismissalWaitGroup sync.WaitGroup InputsBuffer *battle.RingBuffer // Indices are STRICTLY consecutive - InputsBufferLock sync.Mutex // Guards [InputsBuffer, LatestPlayerUpsyncedInputFrameId, LastAllConfirmedInputFrameId, LastAllConfirmedInputList, LastAllConfirmedInputFrameIdWithChange, LastIndividuallyConfirmedInputList, player.LastReceivedInputFrameId] + InputsBufferLock sync.Mutex // Guards [InputsBuffer, LatestPlayerUpsyncedInputFrameId, LastAllConfirmedInputFrameId, LastAllConfirmedInputList, LastAllConfirmedInputFrameIdWithChange, LastIndividuallyConfirmedInputList, player.LastReceivedInputFrameId, player.LastUdpReceivedInputFrameId] RenderFrameBuffer *battle.RingBuffer // Indices are STRICTLY consecutive LatestPlayerUpsyncedInputFrameId int32 LastAllConfirmedInputFrameId int32 @@ -189,6 +189,7 @@ func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, session *websocke pPlayerFromDbInit.AckingInputFrameId = -1 pPlayerFromDbInit.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED pPlayerFromDbInit.LastReceivedInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED + pPlayerFromDbInit.LastUdpReceivedInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED pPlayerFromDbInit.BattleState = PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK pPlayerFromDbInit.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded @@ -230,6 +231,7 @@ func (pR *Room) ReAddPlayerIfPossible(pTmpPlayerInstance *Player, session *webso pEffectiveInRoomPlayerInstance.AckingFrameId = -1 pEffectiveInRoomPlayerInstance.AckingInputFrameId = -1 pEffectiveInRoomPlayerInstance.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED + // [WARNING] DON'T reset "player.LastReceivedInputFrameId" & "player.LastUdpReceivedInputFrameId" upon reconnection! pEffectiveInRoomPlayerInstance.BattleState = PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK pEffectiveInRoomPlayerInstance.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded @@ -1170,6 +1172,7 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr continue } if clientInputFrameId < player.LastReceivedInputFrameId { + // [WARNING] It's important for correctness that we use "player.LastReceivedInputFrameId" instead of "player.LastUdpReceivedInputFrameId" here! Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync#2: roomId=%v, playerId=%v, clientInputFrameId=%v, playerLastReceivedInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, player.LastReceivedInputFrameId, pR.InputsBufferString(false))) continue } @@ -1183,12 +1186,23 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr targetInputFrameDownsync.ConfirmedList |= uint64(1 << uint32(player.JoinIndex-1)) if false == fromUDP { - // [WARNING] We have to distinguish whether or not the incoming batch is from UDP here, otherwise "pR.LatestPlayerUpsyncedInputFrameId - pR.LastAllConfirmedInputFrameId" might become unexpectedly large in case of "UDP packet loss + slow ws session"! + /* + [WARNING] We have to distinguish whether or not the incoming batch is from UDP here, otherwise "pR.LatestPlayerUpsyncedInputFrameId - pR.LastAllConfirmedInputFrameId" might become unexpectedly large in case of "UDP packet loss + slow ws session"! + + Moreover, only ws session upsyncs should advance "player.LastReceivedInputFrameId" & "pR.LatestPlayerUpsyncedInputFrameId". + + Kindly note that the updates of "player.LastReceivedInputFrameId" could be discrete before and after reconnection. + */ player.LastReceivedInputFrameId = clientInputFrameId if clientInputFrameId > pR.LatestPlayerUpsyncedInputFrameId { pR.LatestPlayerUpsyncedInputFrameId = clientInputFrameId } - // It's safe (in terms of getting an eventually correct "RenderFrameBuffer") to put the following update of "pR.LastIndividuallyConfirmedInputList" which is ONLY used for prediction in "InputsBuffer" out of "false == fromUDP" block, but I'm still putting it in for convenient debugging. + } + + if clientInputFrameId > player.LastUdpReceivedInputFrameId { + // No need to update "player.LastUdpReceivedInputFrameId" only when "true == fromUDP", we should keep "player.LastUdpReceivedInputFrameId >= player.LastReceivedInputFrameId" at any moment. + player.LastUdpReceivedInputFrameId = clientInputFrameId + // It's safe (in terms of getting an eventually correct "RenderFrameBuffer") to put the following update of "pR.LastIndividuallyConfirmedInputList" which is ONLY used for prediction in "InputsBuffer" out of "false == fromUDP" block. pR.LastIndividuallyConfirmedInputList[player.JoinIndex-1] = inputFrameUpsync.Encoded } } @@ -1256,6 +1270,7 @@ func (pR *Room) forceConfirmationIfApplicable(prevRenderFrameId int32) uint64 { totPlayerCnt := uint32(pR.Capacity) allConfirmedMask := uint64((1 << totPlayerCnt) - 1) unconfirmedMask := uint64(0) + // As "pR.LastAllConfirmedInputFrameId" can be advanced by UDP but "pR.LatestPlayerUpsyncedInputFrameId" could only be advanced by ws session, when the following condition is met we know that the slow ticker is really in trouble! if pR.LatestPlayerUpsyncedInputFrameId > (pR.LastAllConfirmedInputFrameId + pR.InputFrameUpsyncDelayTolerance + 1) { // Type#1 check whether there's a significantly slow ticker among players oldLastAllConfirmedInputFrameId := pR.LastAllConfirmedInputFrameId diff --git a/frontend/assets/scripts/Map.js b/frontend/assets/scripts/Map.js index c988072..f743225 100644 --- a/frontend/assets/scripts/Map.js +++ b/frontend/assets/scripts/Map.js @@ -46,7 +46,7 @@ window.onUdpMessage = (args) => { const renderedInputFrameIdUpper = gopkgs.ConvertToDelayedInputFrameId(self.renderFrameId); const peerJoinIndex = req.joinIndex; const batch = req.inputFrameUpsyncBatch; - self.onPeerInputFrameUpsync(peerJoinIndex, batch); + self.onPeerInputFrameUpsync(peerJoinIndex, batch, true); } } }; @@ -166,16 +166,10 @@ cc.Class({ return [previousSelfInput, existingInputFrame.InputList[joinIndex - 1]]; } - const lastAllConfirmedInputFrame = self.recentInputCache.GetByFrameId(self.lastAllConfirmedInputFrameId); const prefabbedInputList = new Array(self.playerRichInfoDict.size).fill(0); // the returned "gopkgs.NewInputFrameDownsync.InputList" is immutable, thus we can only modify the values in "prefabbedInputList" for (let k in prefabbedInputList) { - if (null != previousInputFrameDownsync) { - prefabbedInputList[k] = previousInputFrameDownsync.InputList[k]; - } - if (0 <= self.lastAllConfirmedInputFrameId && inputFrameId - 1 > self.lastAllConfirmedInputFrameId) { - prefabbedInputList[k] = lastAllConfirmedInputFrame.InputList[k]; - } + prefabbedInputList[k] = self.lastIndividuallyConfirmedInputList[k]; // Don't predict "btnA & btnB"! prefabbedInputList[k] = (prefabbedInputList[k] & 15); } @@ -355,6 +349,8 @@ cc.Class({ self.lastUpsyncInputFrameId = -1; self.chaserRenderFrameId = -1; // at any moment, "chaserRenderFrameId <= renderFrameId", but "chaserRenderFrameId" would fluctuate according to "onInputFrameDownsyncBatch" + self.lastIndividuallyConfirmedInputFrameId = new Array(window.boundRoomCapacity).fill(-1); + self.lastIndividuallyConfirmedInputList = new Array(window.boundRoomCapacity).fill(0); self.recentRenderCache = new RingBuffer(self.renderCacheSize); self.recentInputCache = gopkgs.NewRingBufferJs((self.renderCacheSize >> 1) + 1); @@ -814,6 +810,16 @@ cc.Class({ return true; }, + _markConfirmationIfApplicable() { + const self = this; + while (self.recentInputCache.StFrameId <= self.lastAllConfirmedInputFrameId && self.lastAllConfirmedInputFrameId < self.recentInputCache.EdFrameId) { + const inputFrameDownsync = self.recentInputCache.GetByFrameId(self.lastAllConfirmedInputFrameId); + if (null == inputFrameDownsync) break; + if (self._allConfirmed(inputFrameDownsync.ConfirmedList)) break; + ++self.lastAllConfirmedInputFrameId; + } + }, + onInputFrameDownsyncBatch(batch /* []*pb.InputFrameDownsync */ ) { // TODO: find some kind of synchronization mechanism against "getOrPrefabInputFrameUpsync"! if (null == batch) { @@ -835,8 +841,6 @@ cc.Class({ if (inputFrameDownsyncId <= self.lastAllConfirmedInputFrameId) { continue; } - // [WARNING] Take all "inputFrameDownsync" from backend as all-confirmed, it'll be later checked by "rollbackAndChase". - self.lastAllConfirmedInputFrameId = inputFrameDownsyncId; const localInputFrame = self.recentInputCache.GetByFrameId(inputFrameDownsyncId); if (null != localInputFrame && @@ -846,14 +850,23 @@ cc.Class({ ) { firstPredictedYetIncorrectInputFrameId = inputFrameDownsyncId; } + // [WARNING] Take all "inputFrameDownsync" from backend as all-confirmed, it'll be later checked by "rollbackAndChase". inputFrameDownsync.confirmedList = (1 << self.playerRichInfoDict.size) - 1; const inputFrameDownsyncLocal = gopkgs.NewInputFrameDownsync(inputFrameDownsync.inputFrameId, inputFrameDownsync.inputList, inputFrameDownsync.confirmedList); // "battle.InputFrameDownsync" in "jsexport" + for (let j in self.playerRichInfoArr) { + const jj = parseInt(j); + if (inputFrameDownsync.inputFrameId > self.lastIndividuallyConfirmedInputFrameId[jj]) { + self.lastIndividuallyConfirmedInputFrameId[jj] = inputFrameDownsync.inputFrameId; + self.lastIndividuallyConfirmedInputList[jj] = inputFrameDownsync.inputList[jj]; + } + } //console.log(`Confirmed inputFrameId=${inputFrameDownsync.inputFrameId}`); const [ret, oldStFrameId, oldEdFrameId] = self.recentInputCache.SetByFrameId(inputFrameDownsyncLocal, inputFrameDownsync.inputFrameId); if (window.RING_BUFF_FAILED_TO_SET == ret) { throw `Failed to dump input cache (maybe recentInputCache too small)! inputFrameDownsync.inputFrameId=${inputFrameDownsync.inputFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}; recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`; } } + self._markConfirmationIfApplicable(); if (null == firstPredictedYetIncorrectInputFrameId) return; const renderFrameId1 = gopkgs.ConvertToFirstUsedRenderFrameId(firstPredictedYetIncorrectInputFrameId) - 1; @@ -879,7 +892,7 @@ batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inpu self.networkDoctor.logRollbackFrames(self.renderFrameId - self.chaserRenderFrameId); }, - onPeerInputFrameUpsync(peerJoinIndex, batch /* []*pb.InputFrameDownsync */ ) { + onPeerInputFrameUpsync(peerJoinIndex, batch, fromUDP) { // TODO: find some kind of synchronization mechanism against "getOrPrefabInputFrameUpsync"! // See `/ConcerningEdgeCases.md` for why this method exists. if (null == batch) { @@ -897,32 +910,39 @@ batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inpu //console.log(`Received peer inputFrameUpsync batch w/ inputFrameId in [${batch[0].inputFrameId}, ${batch[batch.length - 1].inputFrameId}] for prediction assistance`); const renderedInputFrameIdUpper = gopkgs.ConvertToDelayedInputFrameId(self.renderFrameId); for (let k in batch) { - const inputFrameDownsync = batch[k]; - const inputFrameDownsyncId = inputFrameDownsync.inputFrameId; - if (inputFrameDownsyncId < renderedInputFrameIdUpper) { + const inputFrame = batch[k]; // could be either "pb.InputFrameDownsync" or "pb.InputFrameUpsync", depending on "fromUDP" + const inputFrameId = inputFrame.inputFrameId; + if (inputFrameId < renderedInputFrameIdUpper) { // Avoid obfuscating already rendered history continue; } - if (inputFrameDownsyncId <= self.lastAllConfirmedInputFrameId) { + if (inputFrameId <= self.lastAllConfirmedInputFrameId) { + // [WARNING] Don't reject it by "inputFrameId <= self.lastIndividuallyConfirmedInputFrameId[peerJoinIndex-1]", the arrival of UDP packets might not reserve their sending order! continue; } - self.getOrPrefabInputFrameUpsync(inputFrameDownsyncId); // Make sure that inputFrame exists locally - const existingInputFrame = self.recentInputCache.GetByFrameId(inputFrameDownsyncId); + self.getOrPrefabInputFrameUpsync(inputFrameId); // Make sure that inputFrame exists locally + const existingInputFrame = self.recentInputCache.GetByFrameId(inputFrameId); if (0 < (existingInputFrame.ConfirmedList & (1 << (peerJoinIndex - 1)))) { continue; } + const peerEncodedInput = (true == fromUDP ? inputFrame.encoded : inputFrame.inputList[peerJoinIndex - 1]); + if (inputFrameId > self.lastIndividuallyConfirmedInputFrameId[peerJoinIndex - 1]) { + self.lastIndividuallyConfirmedInputFrameId[peerJoinIndex - 1] = inputFrameId; + self.lastIndividuallyConfirmedInputList[peerJoinIndex - 1] = peerEncodedInput; + } effCnt += 1; // the returned "gopkgs.NewInputFrameDownsync.InputList" is immutable, thus we can only modify the values in "newInputList" and "newConfirmedList"! let newInputList = new Array(self.playerRichInfoDict.size).fill(0); for (let i in existingInputFrame.InputList) { newInputList[i] = existingInputFrame.InputList[i]; } + newInputList[peerJoinIndex - 1] = peerEncodedInput; let newConfirmedList = (existingInputFrame.confirmedList | (1 << (peerJoinIndex - 1))); - // No need to change "lastAllConfirmedInputFrameId", leave it to "onInputFrameDownsyncBatch" -- we're just helping prediction here - const newInputFrameDownsyncLocal = gopkgs.NewInputFrameDownsync(inputFrameDownsyncId, newInputList, newConfirmedList); - self.recentInputCache.SetByFrameId(newInputFrameDownsyncLocal, inputFrameDownsyncId); + const newInputFrameDownsyncLocal = gopkgs.NewInputFrameDownsync(inputFrameId, newInputList, newConfirmedList); + self.recentInputCache.SetByFrameId(newInputFrameDownsyncLocal, inputFrameId); } if (0 < effCnt) { + self._markConfirmationIfApplicable(); self.networkDoctor.logPeerInputFrameUpsync(batch[0].inputFrameId, batch[batch.length - 1].inputFrameId); } }, diff --git a/frontend/assets/scripts/WsSessionMgr.js b/frontend/assets/scripts/WsSessionMgr.js index af491a8..fecc456 100644 --- a/frontend/assets/scripts/WsSessionMgr.js +++ b/frontend/assets/scripts/WsSessionMgr.js @@ -335,7 +335,7 @@ window.initSecondarySession = function(onopenCb, boundRoomId) { //console.log(`Got non-empty onmessage decoded: resp.act=${resp.act}`); switch (resp.act) { case window.DOWNSYNC_MSG_ACT_PEER_INPUT_BATCH: - mapIns.onPeerInputFrameUpsync(resp.peerJoinIndex, resp.inputFrameDownsyncBatch); + mapIns.onPeerInputFrameUpsync(resp.peerJoinIndex, resp.inputFrameDownsyncBatch, false); break; default: break;