Syncing the Contact Buffer in Small Chunks for Delta Updates #1254
Replies: 6 comments 8 replies
-
Hello, I'm super busy this week, so I haven't had much time to think about this. I should be able to take a look at this next week. In the mean time: The contact cache only contains contacts for active objects, so I'm not so sure you can get away with partially synching it as I expect a large part of it to change from frame to frame. Can you describe exactly how the partial sync would work? Is the bottleneck that the buffer needs to be synched across the network? Or is it a CPU issue where it takes too much time to save the entire buffer? |
Beta Was this translation helpful? Give feedback.
-
Hello, I'm still having a hard time visualizing how you would be able to send partial snapshots. You're probably filtering out the 'destructibles' through the various functions in For this particular problem, we could perhaps have special functionality that allows you to selectively copy entries from the previous contact cache (i.e. everything that involves a 'destructible'). We don't care about determinism for these objects, so that should be fine. If I understand you correctly, you also want to send a subset of the 'synchronized' objects. And this is where I don't really see how that could work. Determinism is only achieved when the contact cache is 100% the same at the beginning of the time step. If you were to copy the contact cache from the previous time step and then apply a partial delta on top of that, you run the risk of retaining a contact in the cache that had been removed on the server, which would break determinism. If anything changed in objects that you did not send, you would also break determinism. To me it sounds you should not attempt to send a partial snapshot, but try to detect parts of the shapshot that didn't change. This could be done by 'diffing' the previous frame's snapshot with the current and sending that 'diff' over the wire. To help the diffing algorithm, Jolt could tell the |
Beta Was this translation helpful? Give feedback.
-
Hello,
To explain the synchronization mechanism, I need to start by clarifying that each generated snapshot is stored with an index that allows it to be recalled, and also informs us about the execution time. Applying the snapshot is the first step in rewinding and re-synchronizing the client. This process only occurs when the state of an object differs between the server and the client. The rewinding process consists of two phases:
Since some objects are more important for synchronization—think of the ball in Rocket League compared to the rest—we need a method to sync data for the ball without generating a snapshot that includes all objects' information. This way, we can synchronize the ball at a much higher framerate than other objects. ⭐ When the client receives the partial snapshot from the server, as you mentioned, before applying it, we need to merge it with the snapshot generated by the client so that all client-simulated objects can be brought back in time. The client then selects the locally generated snapshot using the index from the partial snapshot just received, and the two are merged to form a single snapshot, correctly restoring the state of ALL objects at that specific moment in time. After applying the snapshot, the inputs are re-executed one by one, and the re-synchronization (rewinding) operation is completed.
What you mentioned is correct—determinism is not guaranteed unless the entire snapshot is applied. However, syncing a snapshot only once per second often results in very noticeable corrections, which can cause issues. The partial snapshot system is designed to reduce the severity of these corrections and simplify the error masking process. This system doesn't eliminate full synchronization, which would still occur. To recap, we’ll have a partial snapshot running at 30Hz and a full snapshot running at 1Hz or slightly higher. This will minimize the error (the difference between the objects' current positions and those on the server) as much as possible.
This is something that we will need to address at later stage to optimize sent buffer. The problemThe issue is that Jolt doesn't allow merging two contact buffers, which is necessary to combine the server's partial snapshot with the local one, as explained here: ⭐ The SolutionI believe the best solution is the one you suggested—enabling the ability to copy specific parts of the contact buffer. This would allow me to apply the server’s partial snapshot after the client’s, while keeping the contact buffer intact for destructible objects that aren’t part of the synchronized simulation. We just need to ensure this doesn’t require additional metadata or redundant information, which would increase the snapshot size. |
Beta Was this translation helpful? Give feedback.
-
I've been thinking about this for a while. I'd prefer not to directly expose all the body pair contacts as the lock free hash set that is being used is quite restrictive. Instead I've created a very simple change that I hope will solve your issue: As long as you have disjoint sets of bodies, constraints and contacts, you can use the The unit tests contain an example. Can you let me know if this indeed works for you? |
Beta Was this translation helpful? Give feedback.
-
Hello Jorrit, Apologies for the long delay. I've been sidetracked with other tasks, but I’m fully focused on this now and can respond quickly. I’ve reviewed the code you proposed, and I really like it. The only issue is that I can’t restore the same contacts twice without causing undefined behavior. This complicates things since the contact buffer is a black box for my application, and I can't filter out contacts that were already applied when restoring the two buffers. This is problematic because the client snapshots all synchronizing bodies and doesn’t know which bodies the server will include in the contact buffer. A potential solution could be to add two functions to the
With these functions, I’d be able to skip restoring contacts that were already applied, avoiding undefined behavior or asserts. Does it sounds ok to you? If so, I can PR your branch with such change. |
Beta Was this translation helpful? Give feedback.
-
Doesn't this get you into trouble? If you use the contact cache of the server and the body configuration of the client, you're not going to be applying the correct contact impulses. This will at least lead to a non-deterministic simulation and it is probably also not physically correct. |
Beta Was this translation helpful? Give feedback.
-
Hello @jrouwe,
I'm integrating a delta update feature to sync the contact buffer across multiple frames. This is necessary because the Contact array is large and creates a bottleneck in the number of objects we can simulate.
Due to the current SaveState implementation, which creates a black-box type buffer, merging two buffers on the client for a single RestoreState call is impossible. The solution I’ve found involves generating partial buffers on the server and sending them to the client:
Once the client receives all partial buffers, instead of merging, it calls RestoreState multiple times:
The problem is that RestoreState clears the previous contacts with this function:
I have some ideas on fixing this, but I’d appreciate your thoughts before proceeding. Do you have any suggestions on how we could address this?
Beta Was this translation helpful? Give feedback.
All reactions