diff --git a/com.unity.multiplayer.mlapi/Runtime/Configuration/NetworkConstants.cs b/com.unity.multiplayer.mlapi/Runtime/Configuration/NetworkConstants.cs index 3222f27f78..f41709d6d2 100644 --- a/com.unity.multiplayer.mlapi/Runtime/Configuration/NetworkConstants.cs +++ b/com.unity.multiplayer.mlapi/Runtime/Configuration/NetworkConstants.cs @@ -24,6 +24,7 @@ internal static class NetworkConstants internal const byte NAMED_MESSAGE = 22; internal const byte SERVER_LOG = 23; internal const byte SNAPSHOT_DATA = 25; + internal const byte SNAPSHOT_ACK = 26; internal const byte SERVER_RPC = 30; internal const byte CLIENT_RPC = 31; internal const byte INVALID = 32; @@ -56,7 +57,7 @@ internal static class NetworkConstants "SERVER_LOG", "", "SNAPSHOT_DATA", - "", + "SNAPSHOT_ACK", "", "", "", diff --git a/com.unity.multiplayer.mlapi/Runtime/Core/IndexAllocator.cs b/com.unity.multiplayer.mlapi/Runtime/Core/IndexAllocator.cs new file mode 100644 index 0000000000..0d3a76849a --- /dev/null +++ b/com.unity.multiplayer.mlapi/Runtime/Core/IndexAllocator.cs @@ -0,0 +1,388 @@ +using UnityEngine; + +namespace MLAPI +{ + internal struct IndexAllocatorEntry + { + internal int Pos; // Position where the memory of this slot is + internal int Length; // Length of the memory allocated to this slot + internal int Next; // Next and Prev define the order of the slots in the buffer + internal int Prev; + internal bool Free; // Whether this is a free slot + } + + internal class IndexAllocator + { + private const int k_NotSet = -1; + private readonly int m_MaxSlot; // Maximum number of sections (free or not) in the buffer + private readonly int m_BufferSize; // Size of the buffer we allocated into + private int m_LastSlot = 0; // Last allocated slot + private IndexAllocatorEntry[] m_Slots; // Array of slots + private int[] m_IndexToSlot; // Mapping from the client's index to the slot index + + internal IndexAllocator(int bufferSize, int maxSlot) + { + m_MaxSlot = maxSlot; + m_BufferSize = bufferSize; + m_Slots = new IndexAllocatorEntry[m_MaxSlot]; + m_IndexToSlot = new int[m_MaxSlot]; + Reset(); + } + + /// + /// Reset this IndexAllocator to an empty one, with the same sized buffer and slots + /// + internal void Reset() + { + // todo: could be made faster, for example by having a last index + // and not needing valid stuff past it + for (int i = 0; i < m_MaxSlot; i++) + { + m_Slots[i].Free = true; + m_Slots[i].Next = i + 1; + m_Slots[i].Prev = i - 1; + m_Slots[i].Pos = m_BufferSize; + m_Slots[i].Length = 0; + + m_IndexToSlot[i] = k_NotSet; + } + + m_Slots[0].Pos = 0; + m_Slots[0].Length = m_BufferSize; + m_Slots[0].Prev = k_NotSet; + m_Slots[m_MaxSlot - 1].Next = k_NotSet; + } + + /// + /// Returns the amount of memory used + /// + /// + /// Returns the amount of memory used, starting at 0, ending after the last used slot + /// + internal int Range + { + get + { + // when the whole buffer is free, m_LastSlot points to an empty slot + if (m_Slots[m_LastSlot].Free) + { + return 0; + } + // otherwise return the end of the last slot used + return m_Slots[m_LastSlot].Pos + m_Slots[m_LastSlot].Length; + } + } + + /// + /// Allocate a slot with "size" position, for index "index" + /// + /// The client index to identify this. Used in Deallocate to identify which slot + /// The size required. + /// Returns the position to use in the buffer + /// + /// true if successful, false is there isn't enough memory available or no slots are large enough + /// + internal bool Allocate(int index, int size, out int pos) + { + pos = 0; + // size must be positive, index must be within range + if (size < 0 || index < 0 || index >= m_MaxSlot) + { + return false; + } + + // refuse allocation if the index is already in use + if (m_IndexToSlot[index] != k_NotSet) + { + return false; + } + + // todo: this is the slowest part + // improvement 1: list of free blocks (minor) + // improvement 2: heap of free blocks + for (int i = 0; i < m_MaxSlot; i++) + { + if (m_Slots[i].Free && m_Slots[i].Length >= size) + { + m_IndexToSlot[index] = i; + + int leftOver = m_Slots[i].Length - size; + int next = m_Slots[i].Next; + if (m_Slots[next].Free) + { + m_Slots[next].Pos -= leftOver; + m_Slots[next].Length += leftOver; + } + else + { + int add = MoveSlotAfter(i); + + m_Slots[add].Pos = m_Slots[i].Pos + size; + m_Slots[add].Length = m_Slots[i].Length - size; + } + + m_Slots[i].Free = false; + m_Slots[i].Length = size; + + pos = m_Slots[i].Pos; + + // if we allocate past the current range, we are the last slot + if (m_Slots[i].Pos + m_Slots[i].Length > Range) + { + m_LastSlot = i; + } + + return true; + } + } + + return false; + } + + /// + /// Deallocate a slot + /// + /// The client index to identify this. Same index used in Allocate + /// + /// true if successful, false is there isn't an allocated slot at this index + /// + internal bool Deallocate(int index) + { + // size must be positive, index must be within range + if (index < 0 || index >= m_MaxSlot) + { + return false; + } + + int slot = m_IndexToSlot[index]; + + if (slot == k_NotSet) + { + return false; + } + + if (m_Slots[slot].Free) + { + return false; + } + + m_Slots[slot].Free = true; + + int prev = m_Slots[slot].Prev; + int next = m_Slots[slot].Next; + + // if previous slot was free, merge and grow + if (prev != k_NotSet && m_Slots[prev].Free) + { + m_Slots[prev].Length += m_Slots[slot].Length; + m_Slots[slot].Length = 0; + + // if the slot we're merging was the last one, the last one is now the one we merged with + if (slot == m_LastSlot) + { + m_LastSlot = prev; + } + + // todo: verify what this does on full or nearly full cases + MoveSlotToEnd(slot); + slot = prev; + } + + next = m_Slots[slot].Next; + + // merge with next slot if it is free + if (next != k_NotSet && m_Slots[next].Free) + { + m_Slots[slot].Length += m_Slots[next].Length; + m_Slots[next].Length = 0; + MoveSlotToEnd(next); + } + + // if we just deallocate the last one, we need to move last back + if (slot == m_LastSlot) + { + m_LastSlot = m_Slots[m_LastSlot].Prev; + // if there's nothing allocated anymore, use 0 + if (m_LastSlot == k_NotSet) + { + m_LastSlot = 0; + } + } + + // mark the index as available + m_IndexToSlot[index] = k_NotSet; + + return true; + } + + // Take a slot at the end and link it to go just after "slot". + // Used when allocating part of a slot and we need an entry for the rest + // Returns the slot that was picked + private int MoveSlotAfter(int slot) + { + int ret = m_Slots[m_MaxSlot - 1].Prev; + int p0 = m_Slots[ret].Prev; + + m_Slots[p0].Next = m_MaxSlot - 1; + m_Slots[m_MaxSlot - 1].Prev = p0; + + int p1 = m_Slots[slot].Next; + m_Slots[slot].Next = ret; + m_Slots[p1].Prev = ret; + + m_Slots[ret].Prev = slot; + m_Slots[ret].Next = p1; + + return ret; + } + + // Move the slot "slot" to the end of the list. + // Used when merging two slots, that gives us an extra entry at the end + private void MoveSlotToEnd(int slot) + { + // if we're already there + if (m_Slots[slot].Next == k_NotSet) + { + return; + } + + int prev = m_Slots[slot].Prev; + int next = m_Slots[slot].Next; + + m_Slots[prev].Next = next; + if (next != k_NotSet) + { + m_Slots[next].Prev = prev; + } + + int p0 = m_Slots[m_MaxSlot - 1].Prev; + + m_Slots[p0].Next = slot; + m_Slots[slot].Next = m_MaxSlot - 1; + + m_Slots[m_MaxSlot - 1].Prev = slot; + m_Slots[slot].Prev = p0; + + m_Slots[slot].Pos = m_BufferSize; + } + + // runs a bunch of consistency check on the Allocator + internal bool Verify() + { + int pos = k_NotSet; + int count = 0; + int total = 0; + int endPos = 0; + + do + { + int prev = pos; + if (pos != k_NotSet) + { + pos = m_Slots[pos].Next; + if (pos == k_NotSet) + { + break; + } + } + else + { + pos = 0; + } + + if (m_Slots[pos].Prev != prev) + { + // the previous is not correct + return false; + } + + if (m_Slots[pos].Length < 0) + { + // Length should be positive + return false; + } + + if (prev != k_NotSet && m_Slots[prev].Free && m_Slots[pos].Free && m_Slots[pos].Length > 0) + { + // should not have two consecutive free slots + return false; + } + + if (m_Slots[pos].Pos != total) + { + // slots should all line up nicely + return false; + } + + if (!m_Slots[pos].Free) + { + endPos = m_Slots[pos].Pos + m_Slots[pos].Length; + } + + total += m_Slots[pos].Length; + count++; + + } while (pos != k_NotSet); + + if (count != m_MaxSlot) + { + // some slots were lost + return false; + } + + if (total != m_BufferSize) + { + // total buffer should be accounted for + return false; + } + + if (endPos != Range) + { + // end position should match reported end position + return false; + } + + return true; + } + + // Debug display the allocator structure + internal void DebugDisplay() + { + string logMessage = "IndexAllocator structure\n"; + + bool[] seen = new bool[m_MaxSlot]; + + int pos = 0; + int count = 0; + bool prevEmpty = false; + do + { + seen[pos] = true; + count++; + if (m_Slots[pos].Length == 0 && prevEmpty) + { + // don't display repetitive empty slots + } + else + { + logMessage += string.Format("{0}:{1}, {2} ({3}) \n", m_Slots[pos].Pos, m_Slots[pos].Length, + m_Slots[pos].Free ? "Free" : "Used", pos); + if (m_Slots[pos].Length == 0) + { + prevEmpty = true; + } + else + { + prevEmpty = false; + } + } + + pos = m_Slots[pos].Next; + } while (pos != k_NotSet && !seen[pos]); + + logMessage += string.Format("{0} Total entries\n", count); + + Debug.Log(logMessage); + } + } +} diff --git a/com.unity.multiplayer.mlapi/Runtime/Core/IndexAllocator.cs.meta b/com.unity.multiplayer.mlapi/Runtime/Core/IndexAllocator.cs.meta new file mode 100644 index 0000000000..b7c7632344 --- /dev/null +++ b/com.unity.multiplayer.mlapi/Runtime/Core/IndexAllocator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bd9e1475e8c8e4a6d935fe2409e3bd26 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.multiplayer.mlapi/Runtime/Core/NetworkManager.cs b/com.unity.multiplayer.mlapi/Runtime/Core/NetworkManager.cs index e37d1cf57a..e5ac693a5f 100644 --- a/com.unity.multiplayer.mlapi/Runtime/Core/NetworkManager.cs +++ b/com.unity.multiplayer.mlapi/Runtime/Core/NetworkManager.cs @@ -1214,6 +1214,10 @@ internal void HandleIncomingData(ulong clientId, NetworkChannel networkChannel, case NetworkConstants.SNAPSHOT_DATA: InternalMessageHandler.HandleSnapshot(clientId, messageStream); break; + case NetworkConstants.SNAPSHOT_ACK: + InternalMessageHandler.HandleAck(clientId, messageStream); + break; + case NetworkConstants.CONNECTION_REQUEST: if (IsServer) { diff --git a/com.unity.multiplayer.mlapi/Runtime/Core/SnapshotSystem.cs b/com.unity.multiplayer.mlapi/Runtime/Core/SnapshotSystem.cs index 263f515460..216ab945d0 100644 --- a/com.unity.multiplayer.mlapi/Runtime/Core/SnapshotSystem.cs +++ b/com.unity.multiplayer.mlapi/Runtime/Core/SnapshotSystem.cs @@ -20,6 +20,7 @@ internal struct VariableKey public ulong NetworkObjectId; // the NetworkObjectId of the owning GameObject public ushort BehaviourIndex; // the index of the behaviour in this GameObject public ushort VariableIndex; // the index of the variable in this NetworkBehaviour + public ushort TickWritten; // the network tick at which this variable was set } // Index for a NetworkVariable in our table of variables @@ -27,9 +28,8 @@ internal struct VariableKey internal struct Entry { public VariableKey Key; - public ushort TickWritten; // the network tick at which this variable was set public ushort Position; // the offset in our Buffer - public ushort Length; // the length of the data in Buffer + public ushort Length; // the Length of the data in Buffer public bool Fresh; // indicates entries that were just received public const int NotFound = -1; @@ -43,23 +43,38 @@ internal struct Entry internal class Snapshot { // todo --M1-- functionality to grow these will be needed in a later milestone - private const int k_MaxVariables = 64; - private const int k_BufferSize = 20000; + private const int k_MaxVariables = 2000; + private const int k_BufferSize = 30000; public byte[] Buffer = new byte[k_BufferSize]; - public int FreeMemoryPosition = 0; + internal IndexAllocator Allocator; public Entry[] Entries = new Entry[k_MaxVariables]; public int LastEntry = 0; public MemoryStream Stream; + private NetworkManager m_NetworkManager; + private bool m_TickIndex; + /// /// Constructor /// Allocated a MemoryStream to be reused for this Snapshot /// - public Snapshot() + /// The NetworkManaher this Snapshot uses. Needed upon receive to set Variables + /// Whether this Snapshot uses the tick as an index + public Snapshot(NetworkManager networkManager, bool tickIndex) { Stream = new MemoryStream(Buffer, 0, k_BufferSize); + // we ask for twice as many slots because there could end up being one free spot between each pair of slot used + Allocator = new IndexAllocator(k_BufferSize, k_MaxVariables * 2); + m_NetworkManager = networkManager; + m_TickIndex = tickIndex; + } + + public void Clear() + { + LastEntry = 0; + Allocator.Reset(); } // todo --M1-- @@ -70,11 +85,13 @@ public Snapshot() /// The key we're looking for public int Find(VariableKey key) { + // todo: Add a IEquatable interface for VariableKey. Rely on that instead. for (int i = 0; i < LastEntry; i++) { if (Entries[i].Key.NetworkObjectId == key.NetworkObjectId && Entries[i].Key.BehaviourIndex == key.BehaviourIndex && - Entries[i].Key.VariableIndex == key.VariableIndex) + Entries[i].Key.VariableIndex == key.VariableIndex && + (!m_TickIndex || (Entries[i].Key.TickWritten == key.TickWritten))) { return i; } @@ -86,16 +103,12 @@ public int Find(VariableKey key) /// /// Adds an entry in the table for a new key /// - // todo: change the params to take a Key instead - public int AddEntry(ulong networkObjectId, int behaviourIndex, int variableIndex) + public int AddEntry(in VariableKey k) { var pos = LastEntry++; var entry = Entries[pos]; - entry.Key.NetworkObjectId = networkObjectId; - entry.Key.BehaviourIndex = (ushort)behaviourIndex; - entry.Key.VariableIndex = (ushort)variableIndex; - entry.TickWritten = 0; + entry.Key = k; entry.Position = 0; entry.Length = 0; entry.Fresh = false; @@ -104,29 +117,166 @@ public int AddEntry(ulong networkObjectId, int behaviourIndex, int variableIndex return pos; } + /// + /// Write an Entry to send + /// Must match ReadEntry + /// + /// The writer to write the entry to + internal void WriteEntry(NetworkWriter writer, in Entry entry) + { + //todo: major refactor. + // use blittable types and copy variable in memory locally + // only serialize when put on the wire for network transfer + writer.WriteUInt64(entry.Key.NetworkObjectId); + writer.WriteUInt16(entry.Key.BehaviourIndex); + writer.WriteUInt16(entry.Key.VariableIndex); + writer.WriteUInt16(entry.Key.TickWritten); + writer.WriteUInt16(entry.Position); + writer.WriteUInt16(entry.Length); + } + + /// + /// Read a received Entry + /// Must match WriteEntry + /// + /// The readed to read the entry from + internal Entry ReadEntry(NetworkReader reader) + { + Entry entry; + entry.Key.NetworkObjectId = reader.ReadUInt64(); + entry.Key.BehaviourIndex = reader.ReadUInt16(); + entry.Key.VariableIndex = reader.ReadUInt16(); + entry.Key.TickWritten = reader.ReadUInt16(); + entry.Position = reader.ReadUInt16(); + entry.Length = reader.ReadUInt16(); + entry.Fresh = false; + + return entry; + } + /// /// Allocate memory from the buffer for the Entry and update it to point to the right location /// /// The entry to allocate for /// The need size in bytes - public void AllocateEntry(ref Entry entry, long size) + public void AllocateEntry(ref Entry entry, int index, int size) { // todo --M1-- // this will change once we start reusing the snapshot buffer memory // todo: deal with free space // todo: deal with full buffer - entry.Position = (ushort)FreeMemoryPosition; + if (entry.Length > 0) + { + Allocator.Deallocate(index); + } + + int pos; + bool ret = Allocator.Allocate(index, size, out pos); + + if (!ret) + { + //todo: error handling + } + + entry.Position = (ushort)pos; entry.Length = (ushort)size; - FreeMemoryPosition += (int)size; + } + + /// + /// Read the buffer part of a snapshot + /// Must match WriteBuffer + /// The stream is actually a memory stream and we seek to each variable position as we deserialize them + /// + /// The NetworkReader to read our buffer of variables from + /// The stream to read our buffer of variables from + internal void ReadBuffer(NetworkReader reader, Stream snapshotStream) + { + int snapshotSize = reader.ReadUInt16(); + + snapshotStream.Read(Buffer, 0, snapshotSize); + + for (var i = 0; i < LastEntry; i++) + { + if (Entries[i].Fresh && Entries[i].Key.TickWritten > 0) + { + // todo: there might be a race condition here with object reuse. To investigate. + var networkVariable = FindNetworkVar(Entries[i].Key); + + if (networkVariable != null) + { + Stream.Seek(Entries[i].Position, SeekOrigin.Begin); + + // todo: consider refactoring out in its own function to accomodate + // other ways to (de)serialize + // todo --M1-- + // Review whether tick still belong in netvar or in the snapshot table. + networkVariable.ReadDelta(Stream, m_NetworkManager.IsServer); + } + } + + Entries[i].Fresh = false; + } + } + + /// + /// Read the snapshot index from a buffer + /// Stores the entry. Allocates memory if needed. The actual buffer will be read later + /// + /// The reader to read the index from + internal void ReadIndex(NetworkReader reader) + { + Entry entry; + short entries = reader.ReadInt16(); + + for (var i = 0; i < entries; i++) + { + entry = ReadEntry(reader); + entry.Fresh = true; + + int pos = Find(entry.Key); + if (pos == Entry.NotFound) + { + pos = AddEntry(entry.Key); + } + + // if we need to allocate more memory (the variable grew in size) + if (Entries[pos].Length < entry.Length) + { + AllocateEntry(ref entry, pos, entry.Length); + } + + Entries[pos] = entry; + } + } + + /// + /// Helper function to find the NetworkVariable object from a key + /// This will look into all spawned objects + /// + /// The key to search for + private INetworkVariable FindNetworkVar(VariableKey key) + { + var spawnedObjects = m_NetworkManager.SpawnManager.SpawnedObjects; + + if (spawnedObjects.ContainsKey(key.NetworkObjectId)) + { + var behaviour = spawnedObjects[key.NetworkObjectId] + .GetNetworkBehaviourAtOrderIndex(key.BehaviourIndex); + return behaviour.NetworkVariableFields[key.VariableIndex]; + } + + return null; } } public class SnapshotSystem : INetworkUpdateSystem, IDisposable { - private Snapshot m_Snapshot = new Snapshot(); - private Snapshot m_ReceivedSnapshot = new Snapshot(); private NetworkManager m_NetworkManager = NetworkManager.Singleton; + private Snapshot m_Snapshot = new Snapshot(NetworkManager.Singleton, false); + private Dictionary m_ClientReceivedSnapshot = new Dictionary(); + + private ushort m_CurrentTick = 0; /// /// Constructor @@ -155,21 +305,37 @@ public void NetworkUpdate(NetworkUpdateStage updateStage) if (updateStage == NetworkUpdateStage.EarlyUpdate) { - if (m_NetworkManager.IsServer) + var tick = m_NetworkManager.NetworkTickSystem.GetTick(); + + if (tick != m_CurrentTick) { - for (int i = 0; i < m_NetworkManager.ConnectedClientsList.Count; i++) + m_CurrentTick = tick; + if (m_NetworkManager.IsServer) { - var clientId = m_NetworkManager.ConnectedClientsList[i].ClientId; - SendSnapshot(clientId); + for (int i = 0; i < m_NetworkManager.ConnectedClientsList.Count; i++) + { + var clientId = m_NetworkManager.ConnectedClientsList[i].ClientId; + SendSnapshot(clientId); + } } - } - else if (m_NetworkManager.IsConnectedClient) - { - SendSnapshot(m_NetworkManager.ServerClientId); - } + else if (m_NetworkManager.IsConnectedClient) + { + SendSnapshot(m_NetworkManager.ServerClientId); + } + + //m_Snapshot.Allocator.DebugDisplay(); + /* + DebugDisplayStore(m_Snapshot, "Entries"); - // DebugDisplayStore(m_Snapshot, "Entries"); - // DebugDisplayStore(m_ReceivedSnapshot, "Received Entries"); + foreach(var item in m_ClientReceivedSnapshot) + { + DebugDisplayStore(item.Value, "Received Entries " + item.Key); + } + */ + // todo: --M1b-- + // for now we clear our send snapshot because we don't have per-client partial sends + m_Snapshot.Clear(); + } } } @@ -185,12 +351,16 @@ private void SendSnapshot(ulong clientId) // Send the entry index and the buffer where the variables are serialized using (var buffer = PooledNetworkBuffer.Get()) { + using (var writer = PooledNetworkWriter.Get(buffer)) + { + writer.WriteUInt16(m_CurrentTick); + } + WriteIndex(buffer); WriteBuffer(buffer); m_NetworkManager.MessageSender.Send(clientId, NetworkConstants.SNAPSHOT_DATA, NetworkChannel.SnapshotExchange, buffer); - buffer.Dispose(); } } @@ -205,77 +375,11 @@ private void WriteIndex(NetworkBuffer buffer) writer.WriteInt16((short)m_Snapshot.LastEntry); for (var i = 0; i < m_Snapshot.LastEntry; i++) { - WriteEntry(writer, in m_Snapshot.Entries[i]); - } - } - } - - /// - /// Read the snapshot index from a buffer - /// Stores the entry. Allocates memory if needed. The actual buffer will be read later - /// - /// The reader to read the index from - private void ReadIndex(NetworkReader reader) - { - Entry entry; - short entries = reader.ReadInt16(); - - for (var i = 0; i < entries; i++) - { - entry = ReadEntry(reader); - entry.Fresh = true; - - int pos = m_ReceivedSnapshot.Find(entry.Key); - if (pos == Entry.NotFound) - { - pos = m_ReceivedSnapshot.AddEntry(entry.Key.NetworkObjectId, entry.Key.BehaviourIndex, - entry.Key.VariableIndex); - } - - // if we need to allocate more memory (the variable grew in size) - if (m_ReceivedSnapshot.Entries[pos].Length < entry.Length) - { - m_ReceivedSnapshot.AllocateEntry(ref entry, entry.Length); + m_Snapshot.WriteEntry(writer, in m_Snapshot.Entries[i]); } - - m_ReceivedSnapshot.Entries[pos] = entry; } } - /// - /// Write an Entry to send - /// Must match ReadEntry - /// - /// The writer to write the entry to - private void WriteEntry(NetworkWriter writer, in Entry entry) - { - writer.WriteUInt64(entry.Key.NetworkObjectId); - writer.WriteUInt16(entry.Key.BehaviourIndex); - writer.WriteUInt16(entry.Key.VariableIndex); - writer.WriteUInt16(entry.TickWritten); - writer.WriteUInt16(entry.Position); - writer.WriteUInt16(entry.Length); - } - - /// - /// Read a received Entry - /// Must match WriteEntry - /// - /// The readed to read the entry from - private Entry ReadEntry(NetworkReader reader) - { - Entry entry; - entry.Key.NetworkObjectId = reader.ReadUInt64(); - entry.Key.BehaviourIndex = reader.ReadUInt16(); - entry.Key.VariableIndex = reader.ReadUInt16(); - entry.TickWritten = reader.ReadUInt16(); - entry.Position = reader.ReadUInt16(); - entry.Length = reader.ReadUInt16(); - entry.Fresh = false; - - return entry; - } - /// /// Write the buffer of a snapshot /// Must match ReadBuffer @@ -285,43 +389,13 @@ private void WriteBuffer(NetworkBuffer buffer) { using (var writer = PooledNetworkWriter.Get(buffer)) { - writer.WriteUInt16((ushort)m_Snapshot.FreeMemoryPosition); + writer.WriteUInt16((ushort)m_Snapshot.Allocator.Range); } // todo --M1-- // // this sends the whole buffer // we'll need to build a per-client list - buffer.Write(m_Snapshot.Buffer, 0, m_Snapshot.FreeMemoryPosition); - } - - /// - /// Read the buffer part of a snapshot - /// Must match WriteBuffer - /// The stream is actually a memory stream and we seek to each variable position as we deserialize them - /// - /// The NetworkReader to read our buffer of variables from - /// The stream to read our buffer of variables from - private void ReadBuffer(NetworkReader reader, Stream snapshotStream) - { - int snapshotSize = reader.ReadUInt16(); - - snapshotStream.Read(m_ReceivedSnapshot.Buffer, 0, snapshotSize); - - for (var i = 0; i < m_ReceivedSnapshot.LastEntry; i++) - { - if (m_ReceivedSnapshot.Entries[i].Fresh && m_ReceivedSnapshot.Entries[i].TickWritten > 0) - { - var nv = FindNetworkVar(m_ReceivedSnapshot.Entries[i].Key); - - m_ReceivedSnapshot.Stream.Seek(m_ReceivedSnapshot.Entries[i].Position, SeekOrigin.Begin); - - // todo --M1-- - // Review whether tick still belong in netvar or in the snapshot table. - nv.ReadDelta(m_ReceivedSnapshot.Stream, m_NetworkManager.IsServer); - } - - m_ReceivedSnapshot.Entries[i].Fresh = false; - } + buffer.Write(m_Snapshot.Buffer, 0, m_Snapshot.Allocator.Range); } // todo: consider using a Key, instead of 3 ints, if it can be exposed @@ -336,60 +410,88 @@ public void Store(ulong networkObjectId, int behaviourIndex, int variableIndex, k.NetworkObjectId = networkObjectId; k.BehaviourIndex = (ushort)behaviourIndex; k.VariableIndex = (ushort)variableIndex; + k.TickWritten = m_NetworkManager.NetworkTickSystem.GetTick(); int pos = m_Snapshot.Find(k); if (pos == Entry.NotFound) { - pos = m_Snapshot.AddEntry(networkObjectId, behaviourIndex, variableIndex); + pos = m_Snapshot.AddEntry(k); } - // write var into buffer, possibly adjusting entry's position and length + WriteVariableToSnapshot(m_Snapshot, networkVariable, pos); + } + + private void WriteVariableToSnapshot(Snapshot snapshot, INetworkVariable networkVariable, int index) + { + // write var into buffer, possibly adjusting entry's position and Length using (var varBuffer = PooledNetworkBuffer.Get()) { networkVariable.WriteDelta(varBuffer); - if (varBuffer.Length > m_Snapshot.Entries[pos].Length) + if (varBuffer.Length > snapshot.Entries[index].Length) { // allocate this Entry's buffer - m_Snapshot.AllocateEntry(ref m_Snapshot.Entries[pos], varBuffer.Length); + snapshot.AllocateEntry(ref snapshot.Entries[index], index, (int)varBuffer.Length); } - m_Snapshot.Entries[pos].TickWritten = m_NetworkManager.NetworkTickSystem.GetTick(); // Copy the serialized NetworkVariable into our buffer - Buffer.BlockCopy(varBuffer.GetBuffer(), 0, m_Snapshot.Buffer, m_Snapshot.Entries[pos].Position, (int)varBuffer.Length); + Buffer.BlockCopy(varBuffer.GetBuffer(), 0, snapshot.Buffer, snapshot.Entries[index].Position, (int)varBuffer.Length); } } + /// /// Entry point when a Snapshot is received /// This is where we read and store the received snapshot /// + /// /// The stream to read from - public void ReadSnapshot(Stream snapshotStream) + public void ReadSnapshot(ulong clientId, Stream snapshotStream) { + ushort snapshotTick = default; + using (var reader = PooledNetworkReader.Get(snapshotStream)) { - ReadIndex(reader); - ReadBuffer(reader, snapshotStream); + snapshotTick = reader.ReadUInt16(); + + if (!m_ClientReceivedSnapshot.ContainsKey(clientId)) + { + m_ClientReceivedSnapshot[clientId] = new Snapshot(m_NetworkManager, false); + } + var snapshot = m_ClientReceivedSnapshot[clientId]; + + // todo --M1b-- temporary, clear before receive. + snapshot.Clear(); + + snapshot.ReadIndex(reader); + snapshot.ReadBuffer(reader, snapshotStream); } + + SendAck(clientId, snapshotTick); } - /// - /// Helper function to find the NetworkVariable object from a key - /// This will look into all spawned objects - /// - /// The key to search for - private INetworkVariable FindNetworkVar(VariableKey key) + public void ReadAck(ulong clientId, Stream snapshotStream) { - var spawnedObjects = m_NetworkManager.SpawnManager.SpawnedObjects; + using (var reader = PooledNetworkReader.Get(snapshotStream)) + { + var ackTick = reader.ReadUInt16(); + //Debug.Log(string.Format("Receive ack {0} from client {1}", ackTick, clientId)); + } + } - if (spawnedObjects.ContainsKey(key.NetworkObjectId)) + public void SendAck(ulong clientId, ushort tick) + { + using (var buffer = PooledNetworkBuffer.Get()) { - var behaviour = spawnedObjects[key.NetworkObjectId] - .GetNetworkBehaviourAtOrderIndex(key.BehaviourIndex); - return behaviour.NetworkVariableFields[key.VariableIndex]; + using (var writer = PooledNetworkWriter.Get(buffer)) + { + writer.WriteUInt16(tick); + } + + m_NetworkManager.MessageSender.Send(clientId, NetworkConstants.SNAPSHOT_ACK, + NetworkChannel.SnapshotExchange, buffer); + buffer.Dispose(); } - return null; } // todo --M1-- @@ -400,8 +502,8 @@ private void DebugDisplayStore(Snapshot block, string name) string table = "=== Snapshot table === " + name + " ===\n"; for (int i = 0; i < block.LastEntry; i++) { - table += string.Format("NetworkObject {0}:{1}:{2} range [{3}, {4}] ", block.Entries[i].Key.NetworkObjectId, block.Entries[i].Key.BehaviourIndex, - block.Entries[i].Key.VariableIndex, block.Entries[i].Position, block.Entries[i].Position + block.Entries[i].Length); + table += string.Format("NetworkVariable {0}:{1}:{2} written {5}, range [{3}, {4}] ", block.Entries[i].Key.NetworkObjectId, block.Entries[i].Key.BehaviourIndex, + block.Entries[i].Key.VariableIndex, block.Entries[i].Position, block.Entries[i].Position + block.Entries[i].Length, block.Entries[i].Key.TickWritten); for (int j = 0; j < block.Entries[i].Length && j < 4; j++) { diff --git a/com.unity.multiplayer.mlapi/Runtime/Messaging/InternalMessageHandler.cs b/com.unity.multiplayer.mlapi/Runtime/Messaging/InternalMessageHandler.cs index 09fa658c5a..8bc3e25996 100644 --- a/com.unity.multiplayer.mlapi/Runtime/Messaging/InternalMessageHandler.cs +++ b/com.unity.multiplayer.mlapi/Runtime/Messaging/InternalMessageHandler.cs @@ -398,7 +398,12 @@ public void HandleNetworkLog(ulong clientId, Stream stream) internal static void HandleSnapshot(ulong clientId, Stream messageStream) { - NetworkManager.Singleton.SnapshotSystem.ReadSnapshot(messageStream); + NetworkManager.Singleton.SnapshotSystem.ReadSnapshot(clientId, messageStream); + } + + internal static void HandleAck(ulong clientId, Stream messageStream) + { + NetworkManager.Singleton.SnapshotSystem.ReadAck(clientId, messageStream); } public void HandleAllClientsSwitchSceneCompleted(ulong clientId, Stream stream) diff --git a/com.unity.multiplayer.mlapi/Runtime/Transports/NetworkTransport.cs b/com.unity.multiplayer.mlapi/Runtime/Transports/NetworkTransport.cs index 53685f95e6..b7cf3c4a00 100644 --- a/com.unity.multiplayer.mlapi/Runtime/Transports/NetworkTransport.cs +++ b/com.unity.multiplayer.mlapi/Runtime/Transports/NetworkTransport.cs @@ -104,7 +104,7 @@ public TransportChannel[] MLAPI_CHANNELS // todo: Currently, fragmentation support needed to deal with oversize packets encounterable with current pre-snapshot code". // todo: once we have snapshotting able to deal with missing frame, this should be unreliable new TransportChannel(NetworkChannel.NetworkVariable, NetworkDelivery.ReliableFragmentedSequenced), - new TransportChannel(NetworkChannel.SnapshotExchange, NetworkDelivery.Unreliable), + new TransportChannel(NetworkChannel.SnapshotExchange, NetworkDelivery.ReliableFragmentedSequenced), // todo: temporary until we separate snapshots in chunks }; /// diff --git a/com.unity.multiplayer.mlapi/Tests/Editor/IndexAllocatorTests.cs b/com.unity.multiplayer.mlapi/Tests/Editor/IndexAllocatorTests.cs new file mode 100644 index 0000000000..da5610b235 --- /dev/null +++ b/com.unity.multiplayer.mlapi/Tests/Editor/IndexAllocatorTests.cs @@ -0,0 +1,117 @@ +using NUnit.Framework; +using UnityEngine; + +namespace MLAPI.EditorTests +{ + public class FixedAllocatorTest + { + [Test] + public void SimpleTest() + { + int pos; + + var allocator = new IndexAllocator(20000, 200); + allocator.DebugDisplay(); + + // allocate 20 bytes + Assert.IsTrue(allocator.Allocate(0, 20, out pos)); + allocator.DebugDisplay(); + Assert.IsTrue(allocator.Verify()); + + // can't ask for negative amount of memory + Assert.IsFalse(allocator.Allocate(1, -20, out pos)); + Assert.IsTrue(allocator.Verify()); + + // can't ask for deallocation of negative index + Assert.IsFalse(allocator.Deallocate(-1)); + Assert.IsTrue(allocator.Verify()); + + // can't ask for the same index twice + Assert.IsFalse(allocator.Allocate(0, 20, out pos)); + Assert.IsTrue(allocator.Verify()); + + // allocate another 20 bytes + Assert.IsTrue(allocator.Allocate(1, 20, out pos)); + allocator.DebugDisplay(); + Assert.IsTrue(allocator.Verify()); + + // allocate a third 20 bytes + Assert.IsTrue(allocator.Allocate(2, 20, out pos)); + allocator.DebugDisplay(); + Assert.IsTrue(allocator.Verify()); + + // deallocate 0 + Assert.IsTrue(allocator.Deallocate(0)); + allocator.DebugDisplay(); + Assert.IsTrue(allocator.Verify()); + + // deallocate 1 + allocator.Deallocate(1); + allocator.DebugDisplay(); + Assert.IsTrue(allocator.Verify()); + + // deallocate 2 + allocator.Deallocate(2); + allocator.DebugDisplay(); + Assert.IsTrue(allocator.Verify()); + + // allocate 50 bytes + Assert.IsTrue(allocator.Allocate(0, 50, out pos)); + allocator.DebugDisplay(); + Assert.IsTrue(allocator.Verify()); + + // allocate another 50 bytes + Assert.IsTrue(allocator.Allocate(1, 50, out pos)); + allocator.DebugDisplay(); + Assert.IsTrue(allocator.Verify()); + + // allocate a third 50 bytes + Assert.IsTrue(allocator.Allocate(2, 50, out pos)); + allocator.DebugDisplay(); + Assert.IsTrue(allocator.Verify()); + + // deallocate 1, a block in the middle this time + allocator.Deallocate(1); + allocator.DebugDisplay(); + Assert.IsTrue(allocator.Verify()); + + // allocate a smaller one in its place + allocator.Allocate(1, 25, out pos); + allocator.DebugDisplay(); + Assert.IsTrue(allocator.Verify()); + } + + [Test] + public void ReuseTest() + { + int count = 100; + bool[] used = new bool[count]; + int[] pos = new int[count]; + int bufferSize = 20000; + int iterations = 10000; + + var allocator = new IndexAllocator(20000, 200); + + for (int i = 0; i < iterations; i++) + { + int index = Random.Range(0, count); + if (used[index]) + { + Assert.IsTrue(allocator.Deallocate(index)); + used[index] = false; + } + else + { + int position; + int length = 10 * Random.Range(1, 10); + Assert.IsTrue(allocator.Allocate(index, length, out position)); + pos[index] = position; + used[index] = true; + } + Assert.IsTrue(allocator.Verify()); + } + allocator.DebugDisplay(); + } + + } +} diff --git a/com.unity.multiplayer.mlapi/Tests/Editor/IndexAllocatorTests.cs.meta b/com.unity.multiplayer.mlapi/Tests/Editor/IndexAllocatorTests.cs.meta new file mode 100644 index 0000000000..760a4ccfe5 --- /dev/null +++ b/com.unity.multiplayer.mlapi/Tests/Editor/IndexAllocatorTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 85ac488e1432d49668c711fa625a0743 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: