-
Notifications
You must be signed in to change notification settings - Fork 450
feat: snapshot. Milestone 1b. Testproject "manual test" "scene transitioning" working with snapshot. Disabled by default. #862
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…t every frame, having an empty NativeContainer. New channel and messagetype.
…ure around writing and sending the snapshot
…ot to send and the received one
…updated on all machines
…e network manager
…to experimental/snapshot-system-m1
… of snapshot table
… of passing its value around
… of passing its value around
{ | ||
if (Entries[i].Fresh && Entries[i].Key.TickWritten > 0) | ||
{ | ||
var nv = FindNetworkVar(Entries[i].Key); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will fail if a single NetworkObject has changed between the client and the server.
Example:
- Client sends variable for NetworkObject with Id 5 (PlayerPrefab)
- Server Despawns NetworkObjectId with Id 5
- Server spawns (NetworkOtherPrefab), Id 5 gets assigned to that new object.
- This function tries to read the snapshot, searches for a variable which does not exist on "NetworkOtherPrefab"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I was prepared to write a very similar comment, until this popped out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, good point, seems like I made assumptions about uniqueness of Ids which don't hold.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How did that not manifest in existing mlapi ? It seems the whole serialization makes the same assumptions, no ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now this is not handled in MLAPI. There is an id recycling delay which in theory should prevent the above situation in many cases. But that's just one of the cases. Another problem would be if the object just does not exist anymore due to being despawned/destroyed.
I don't think this PR should/can address all those issues. Just wanted to point them out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noted.
@@ -20,16 +20,16 @@ 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This VariableKey
should implement IEquatable<VariableKey>
and override the hashcode to avoid boxing and generally for better performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice trick!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate where the boxing occurs, currently, and how making the VariableKey equatable would help ? Are you suggesting the Find function to rely on this equatable interface ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought that we were using this key in hashsets/dictionary as a key. After looking more closely at the code this seems to not be the case.
The IEquatable interface prevents boxing when doing equality comparisons between two variables of the same type. So variableKey1.Equals(variableKey2)
allocates if VariableKey
is a struct type. Usually it's standard practice to have structs which act as a key implement the interface. Here it is not necessary yet but not implementing the interface could burn us in the future if someone uses this in a dictionary/hashset.
Rider can auto generate the interface just implement Equatable<VariableKey>
and Rider Intelisense will give you then option to generate equality members.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a pretty old branch. Considering that:
- This branch uses
(!m_TickIndex || (Entries[i].Key.TickWritten == key.TickWritten)))
(One snapshot cares about tick, the other doesn't) - The
snapshot-system-spawn
branch has a plain comparison. - Those are not actually used as key.
I'll simply throw a comment for now, and eventually add the IEquatable interface in the spawn branch.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this live inside the actual struct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get your question. Maybe the comment can be improved. This is the length of a specific entry. So, yes, each Entry requires its own length.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ups, I meant the NotFound constant variable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could be in Snapshot
too. But since it is a constant and doesn't contribute to object size, what difference does it make? I kind-of likeEntry.NotFound
as it expresses clearly what it means. How do others feel ?
@@ -74,7 +89,8 @@ public int Find(VariableKey key) | |||
{ | |||
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 && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like it would be pretty straightforward to establish a dictionary and then de-ref the key.
On m_tickIndex, I had to stop to reason about this code. I think I would name it m_TickAsIndex else my brain really wants it to mean "the index of a tick"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so this logic is, if I find a matching entry AND (either I'm not using tick as an index OR the tick written matches the one I'm looking at).
I'm assuming the tick matching part is so that we don't have to clear out old entries? I get that. But I'm not sure why if this isn't true that it's still ok for it to be a hit if m_tickIndex is false.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this (old) branch, we still had different Snapshots per connection. I was swayed, in our discussions, by Luke's very good and valid arguments. As such, the latest branch (spawn) doesn't need this anymore.
But to clarify this code, in order for a Key to match, the NetworkObjectId
, the BehaviourIndex
and the VariableIndex
have to match. Also, you need one of two things to hold:
- either this Snapshot doesn't care about ticks (
!m_TickIndex
) or - the ticks actually match
(Entries[i].Key.TickWritten == key.TickWritten)
This was so that per-connection snapshots would only maintain the latest value of each variable, but the main snapshot would maintain multiple.
I could comment a bit to improve, but since the next branch will drop this, it would be a bit moot.
@@ -86,16 +102,12 @@ public int Find(VariableKey key) | |||
/// <summary> | |||
/// Adds an entry in the table for a new key | |||
/// </summary> | |||
// todo: change the params to take a Key instead | |||
public int AddEntry(ulong networkObjectId, int behaviourIndex, int variableIndex) | |||
public int AddEntry(VariableKey k) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this can be declared 'in'
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit, I might re-name this 'Unprocessed' (assuming that's what it means)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but same comment as much of above. This got removed already from the spawn
branch.
/// Must match WriteEntry | ||
/// </summary> | ||
/// <param name="reader">The readed to read the entry from</param> | ||
internal Entry ReadEntry(NetworkReader reader) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be 'in' modified, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? NetworkReader
is a reference type. I thought in
was meant to improve the performance by not copying value types. If it was an in
parameter, it would only prevent assigning reader
a new NetworkReader. Since the code doesn't do it, since it wouldn't affect the caller if it did, and since there's no performance improvements in doing it, I fail to see the point.
{ | ||
// 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; | ||
int pos; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super nit, put right ahead of first usage on line 175 instead
|
||
if (!ret) | ||
{ | ||
//todo: error handling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe throw an assert in here for the time being? This is a 'game over' situation, yes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you clarify what you would like to see? Something like:
namespace MLAPI.Exceptions
{
public class SnapshotMemoryException : Exception
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I read throw
as in exception. The alternative is indeed just an assert. But since Assert are development build only, I feel we need an alternative way to report and handle errors
|
||
for (var i = 0; i < LastEntry; i++) | ||
{ | ||
if (Entries[i].Fresh && Entries[i].Key.TickWritten > 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have to do a linear search from 0 until we find Fresh
ones? Like could we just have an index into the first Fresh one rather than scan down to it?
For some reason I figured Entries
would be a circular buffer, but maybe instead when we get a new entry we just find the first un-fresh slot and stick it there.
Actually yeah, I never see where LastEntry
decreases except when clear
sets it to zero. I'm consufed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good points. Maybe I was expecting code to prematurely go into develop
and make in in stages.
Once we have acknowledgements, variables will be removed from the main snapshots. When the last variable is removed LastEntry
will decrease back. This PR was only meant for the M1b milestone and getting the scale-demo to pass.
private NetworkManager m_NetworkManager = NetworkManager.Singleton; | ||
private Snapshot m_Snapshot = new Snapshot(NetworkManager.Singleton, false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps rename to m_ServerSnapshot?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. But the spawn
version is now using a single snapshot, so the naming might not be super important. :-)
Also, when it doesn't come with extra cost, I try to abstract away the difference between server and client. Maybe one day we'll end up with hybrid combination: partial meshes, connection between hosts, etc... Then, the less "server"- and "client"-specific code we have, the most flexible it should be.
|
||
if (!m_ClientReceivedSnapshot.ContainsKey(clientId)) | ||
{ | ||
m_ClientReceivedSnapshot[clientId] = new Snapshot(m_NetworkManager, false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't there be a corresponding removal from m_ClientReceivedSnapshot
when a client disconnects? (I know it wouldn't happen here, but seems like we need to listen for client disconnects or something to sweep these out)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is another part that goes away in the spawn
branch. Since there's a single snapshot, we don't allocate per client.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clarifications / renames / a few concerns
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think there is anything critical anymore preventing us from merging this. All Code looks good or has a comment explaining the WIP nature of it.
…nsform * develop: feat: snapshot. Adding RTT computation API. (#963) feat: snapshot. Milestone 1b. Testproject "manual test" "scene transitioning" working with snapshot. Disabled by default. (#862) test: multiprocess tests part 6: fixing issues runnings all tests together (#957) docs: Perf tests part 5. Adding documentation and instructions (#952) test: Perf tests part 4. Adding example of performance test with spawning x network objects at once (#925) test: Correctly teardown OnNetworkSpawn/Despawn tests.
This updates
develop
with the current Snapshot-System branch. No impact as everything is disabled by default, but keeps the branches close to each other.