Skip to content

Commit 2c7322f

Browse files
chore: review pass over the batched NetworkTransform comments
Six comments asserted something that was not true. None of them fail a test, and none are visible from the code they sit next to, which is what they have in common: every one is a claim about lifetime, ordering, or the other implementation rather than about the lines below it. - NativeInterpolator.ResetTo claimed the managed implementation seeds a baseline measurement and this one does not. Neither seeds one. The divergence it spent thirty lines explaining does not exist, and the explanation that does belong lives once, on BufferedLinearInterpolator.ResetTo. - NetworkTransformStateManager said Register and Deregister are the only places the collections change. There are two independent sets, and the interpolation set changes in its own pair of methods. - The WriteBatch guard said only the server registers instances for batching, which contradicts RegisterForBatchedStateTracking. A client authority does register. What is client-server only is the queuing, and a distributed authority session queues nothing at all. - The handle allocator field said a handle is assigned to every synchronized instance. Only in batched mode. - DetectTransformDeltaJob said every caller forcing a full state update ticks a nested instance. There is one such caller and it is gated on SwitchTransformSpaceWhenParented, which does not exclude an instance from batching. It runs from CommitDetectedState after the job, which is the real reason the job never needs to be told to force. - Two test remarks described designs that had moved: the interpolator reset test described the bug in the present tense, and the state baseline test attributed the ReliableSequenced derivation to NetworkSerialize rather than to UpdateReliability. The rest is verbosity. Remarks that ran to a paragraph per method are now one statement per line, big-O notation is replaced by what actually happens, and the justifications for why the code is correct are gone where the rule alone is enough. serverTime is dropped from the internal interpolator reset path. BufferedLinearInterpolator.ResetTo forwards it into an AddMeasurement call it does not make, so nothing reads it in either implementation. The managed signature is public API and keeps it, with a note to obsolete it later.
1 parent ea4f91f commit 2c7322f

11 files changed

Lines changed: 113 additions & 129 deletions

com.unity.netcode.gameobjects/Runtime/Components/DetectTransformDeltaJob.cs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,31 @@ namespace Unity.Netcode.Components
77
{
88
/// <summary>
99
/// Motion Authority Only:
10-
/// Detects <see cref="NetworkTransform"/> state changes for every registered instance in parallel.
10+
/// Detects <see cref="NetworkTransform"/> state changes for every registered instance in a parallel job.
1111
/// </summary>
1212
/// <remarks>
13-
/// This reads each transform and defers to the very same
1413
/// <see cref="CheckForStateChange(ref NetworkTransformState, ref NetworkDeltaPosition, ref TransformDeltaConfig, in TransformSample, bool, bool, bool)"/>
15-
/// that the per instance path runs on the main thread to keep the logic between per instance and batched the same.<br />
16-
/// Only transform values are read here and only the entries array is written, so there is no hierarchy
17-
/// write hazard: nothing in this job touches a transform other than the one at its own index, and nothing
18-
/// writes to a transform at all.
14+
/// is the common method used for both per instance, runs on the main thread, and batched modes. This assures both paths detect changes in state identically.
1915
/// </remarks>
2016
[BurstCompile]
2117
internal struct DetectTransformDeltaJob : IJobParallelForTransform
2218
{
2319
/// <summary>
24-
/// The per instance input and output, parallel to the transforms this job is scheduled over.
20+
/// The per instance input and output, parallel to the job's scheduled transforms.
2521
/// </summary>
2622
public NativeArray<TransformDeltaEntry> Entries;
2723

24+
/// <summary>
25+
/// This job's primary entry point.
26+
/// </summary>
27+
/// <remarks>
28+
/// TODO: Investigate ways to work around the fact that a Rigidbody's position and rotation cannot
29+
/// be sampled from a job. As such, any NetworkTransform that is using Rigidbody for motion will
30+
/// use the <see cref="UnityEngine.GameObject.transform"/> to detect changes in position and rotation
31+
/// states on the authority side.
32+
/// </remarks>
33+
/// <param name="index">Index for the transform in question.</param>
34+
/// <param name="transform">The job safe <see cref="TransformAccess"/></param>
2835
public void Execute(int index, TransformAccess transform)
2936
{
3037
if (!transform.isValid)
@@ -34,17 +41,14 @@ public void Execute(int index, TransformAccess transform)
3441

3542
var entry = Entries[index];
3643
var flagStates = entry.State.FlagStates;
37-
// Only ResolveTransformSpace can raise this on the batched path: every caller that forces a
38-
// full state update ticks a nested instance, and a nested instance is never registered for
39-
// batching.
44+
// Only ResolveTransformSpace can raise this on the batched path. The one caller that forces a full
45+
// state update does so from CommitDetectedState on the main thread, after this job has run.
4046
var forceState = false;
4147

4248
// Resolve the transform space before sampling, otherwise the wrong set of values gets compared.
4349
var transformSpaceChanged = ResolveTransformSpace(ref entry.Config, ref flagStates, entry.TransformHasParent, false, ref forceState);
4450
entry.State.FlagStates = flagStates;
4551

46-
// A rigidbody driven instance cannot be sampled from here, so it is never registered for the
47-
// batched path and always falls back to the per instance flow.
4852
var rotation = entry.Config.InLocalSpace ? transform.localRotation : transform.rotation;
4953
entry.Sample.Position = entry.Config.InLocalSpace ? transform.localPosition : transform.position;
5054
entry.Sample.Rotation = rotation;

com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,8 @@ public void Clear()
274274
/// Resets the current interpolator to the target value.
275275
/// </summary>
276276
/// <remarks>
277-
/// This is used when first synchronizing/initializing and when telporting an object.
277+
/// This is used when first synchronizing/initializing and when teleporting an object.<br />
278+
/// <paramref name="serverTime"/> is not used. Mark this obsolete and deprecate it at a later date.
278279
/// </remarks>
279280
/// <param name="targetValue">The target value to reset the interpolator to</param>
280281
/// <param name="serverTime">The current server time</param>

com.unity.netcode.gameobjects/Runtime/Components/Interpolator/InterpolateTransformJob.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,15 @@ internal struct InterpolationEntry
5252

5353
/// <summary>
5454
/// Non-Authority Only:
55-
/// Advances the interpolators for every registered non-authority <see cref="NetworkTransform"/> in
56-
/// parallel.
55+
/// Handles interpolation for every registered non-authority <see cref="NetworkTransform"/> in
56+
/// a parallel job.
5757
/// </summary>
5858
/// <remarks>
59-
/// This performs the buffer consumption and the interpolation math only. Applying the results to the
60-
/// transforms stays on the main thread for now (keeps this free of any hierarchy write order of operation complexities).<br />
61-
/// <br />
62-
/// Also, note that each entry owns its own slice of <see cref="BufferedItems"/> which assures no two indices address the same
63-
/// items and that the whole array can be written without aliasing (pointing to the same thing).
59+
/// This performs the buffer consumption and interpolation between two state updates only.<br />
60+
/// Applying the results to the transforms stays on the main thread, which keeps this job free
61+
/// of hierarchy write ordering.<br />
62+
/// Each entry owns its own slice of <see cref="BufferedItems"/>, so no two indices address the same items.<br />
63+
/// The whole array can be written without aliasing (access is to a distinct, independent memory region).
6464
/// </remarks>
6565
[BurstCompile]
6666
internal struct InterpolateTransformJob : IJobParallelFor

com.unity.netcode.gameobjects/Runtime/Components/Interpolator/NativeInterpolator.cs

Lines changed: 14 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -296,37 +296,13 @@ internal static void Reset(ref NativeInterpolatorState state, float4 currentValu
296296
/// <see cref="BufferedLinearInterpolator{T}.ResetTo"/>.
297297
/// </summary>
298298
/// <remarks>
299-
/// The managed implementation seeds a baseline measurement here, stamped with the caller's
300-
/// <paramref name="serverTime"/>. This one deliberately does not, because that baseline becomes an
301-
/// ordering floor that the measurements which follow it cannot clear.<br />
302-
/// <br />
303-
/// Callers pass <c>NetworkManager.ServerTime.Time</c>, the local current time, while incoming
304-
/// measurements are stamped with the tick they were authored on
305-
/// (<see cref="NetworkTransform.NetworkTransformState.SentTime"/>), which is always at least a tick
306-
/// older. Once the baseline is consumed it becomes <see cref="NativeInterpolatorState.Target"/>, and
307-
/// both of the guards that admit a measurement compare against it:
308-
/// <see cref="AddMeasurement"/> requires a stamp newer than
309-
/// <see cref="NativeInterpolatorState.LastMeasurementAddedTime"/>, and
310-
/// <see cref="TryConsumeFromBuffer"/> requires one newer than <c>Target.TimeSent</c>. An instance that
311-
/// resets part way through a session therefore rejects everything the authority sends next, and the
312-
/// rejection is permanent: with a target already reached and a non empty buffer,
313-
/// <see cref="Update"/> neither interpolates nor takes the stale target reset, so elapsed time alone
314-
/// never recovers it.<br />
315-
/// <br />
316-
/// In practice this only reaches an instance that just stopped being the authority, which in a client
317-
/// server topology is only ever the server. Leaving the buffer empty puts the interpolator in exactly
318-
/// the state a freshly spawned one is in: the value is still held (<see cref="Reset"/> seeds all three
319-
/// of the in flight values), the first measurement to arrive is taken unconditionally because
320-
/// <see cref="NativeInterpolatorState.BufferCounter"/> is zero, and it is consumed against render time
321-
/// alone. Seeding at spawn is unaffected — the baseline stamp there is one tick ahead of the first
322-
/// measurement, so the interval the first measurement is interpolated over is the tick length either
323-
/// way.
299+
/// Clears the buffer and holds <paramref name="targetValue"/> as the current value.<br />
300+
/// No baseline measurement is recorded, which is what the managed implementation does as well. See
301+
/// <see cref="BufferedLinearInterpolator{T}.ResetTo(T, double)"/> for why.<br />
302+
/// This leaves the interpolator in the state a freshly spawned one is in, so the next measurement to
303+
/// arrive is taken unconditionally.
324304
/// </remarks>
325-
/// <param name="serverTime">
326-
/// Retained for signature parity with <see cref="BufferedLinearInterpolator{T}.ResetTo"/>. Not stored,
327-
/// for the reason above.
328-
/// </param>
329-
internal static void ResetTo(ref NativeInterpolatorState state, ref NativeArray<BufferedItemNative> items, float4 targetValue, double serverTime)
305+
internal static void ResetTo(ref NativeInterpolatorState state, ref NativeArray<BufferedItemNative> items, float4 targetValue)
330306
{
331307
Clear(ref state);
332308
state.RateOfChange = float4.zero;
@@ -394,20 +370,17 @@ internal static void ResetCurrentState(ref NativeInterpolatorState state)
394370
}
395371

396372
/// <summary>
397-
/// Re-expresses every buffered measurement, and the values currently in flight, in a different space.
373+
/// Re-expresses the buffered measurements and the in flight values in a different transform space.
398374
/// </summary>
399375
/// <remarks>
400-
/// Invoked when the instance is reparented, which is the only thing that changes the space its
401-
/// measurements are interpreted in. Converting the whole buffer at that moment keeps everything in one
402-
/// space, which is what allows the interpolation job to have no knowledge of parents at all.<br />
403-
/// <br />
404-
/// The managed interpolator instead tags each measurement with the parent it arrived under and
405-
/// converts lazily as the queue drains past the boundary. The net effect is the same transition; doing
406-
/// it here means one conversion at a known instant, using both parents' poses as they are at that
407-
/// instant, rather than a conversion per buffered item using poses sampled as each is consumed.
376+
/// Invoked when the instance is reparented. Reparenting is the only thing that changes the
377+
/// transform space for its measurements.<br />
378+
/// Converting everything at that point is what keeps the interpolation job free of any parent
379+
/// knowledge.<br />
380+
/// The managed interpolator converts lazily as its queue drains instead. Both reach the same result.
408381
/// </remarks>
409-
/// <param name="pointTransform">Converts a position from the old space to the new one.</param>
410-
/// <param name="rotationTransform">Converts a rotation from the old space to the new one.</param>
382+
/// <param name="pointTransform">Converts a position from the old transform space to the new one.</param>
383+
/// <param name="rotationTransform">Converts a rotation from the old transform space to the new one.</param>
411384
internal static void ConvertSpace(ref NativeInterpolatorState state, ref NativeArray<BufferedItemNative> items, in float4x4 pointTransform, in quaternion rotationTransform)
412385
{
413386
for (int i = 0; i < state.BufferCount; i++)

com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1789,11 +1789,11 @@ public Vector3 GetScale(bool getCurrentState = false)
17891789
/// <summary>
17901790
/// When <see cref="TransformSyncModes.Batched"/> mode, this is the instance's index within <see cref="NetworkTransformStateManager"/>.<br />
17911791
/// It is -1 when it is not registered.
1792-
/// registered.
17931792
/// </summary>
17941793
/// <remarks>
1795-
/// Cached here (as opposed to using a lookup table) so registering and deregistering is O(1). The
1796-
/// manager keeps this up to date as instances are swapped between slots.
1794+
/// Cached here as opposed to using a lookup table, so registering and deregistering do not have to
1795+
/// search for the instance.<br />
1796+
/// The manager keeps this up to date as instances are swapped between slots.
17971797
/// </remarks>
17981798
internal int StateManagerIndex = -1;
17991799

@@ -2605,7 +2605,7 @@ internal void UpdatePositionInterpolator(Vector3 position, double time, bool res
26052605
var value = new float4(position.x, position.y, position.z, 0.0f);
26062606
if (resetInterpolator)
26072607
{
2608-
m_CachedNetworkManager.TransformStateManager.ResetTo(InterpolatorIndex, NetworkTransformStateManager.InterpolatorTarget.Position, value, time);
2608+
m_CachedNetworkManager.TransformStateManager.ResetTo(InterpolatorIndex, NetworkTransformStateManager.InterpolatorTarget.Position, value);
26092609
}
26102610
else
26112611
{
@@ -2638,7 +2638,7 @@ private void UpdateRotationInterpolator(Quaternion rotation, double time, bool r
26382638
var value = new float4(rotation.x, rotation.y, rotation.z, rotation.w);
26392639
if (resetInterpolator)
26402640
{
2641-
m_CachedNetworkManager.TransformStateManager.ResetTo(InterpolatorIndex, NetworkTransformStateManager.InterpolatorTarget.Rotation, value, time);
2641+
m_CachedNetworkManager.TransformStateManager.ResetTo(InterpolatorIndex, NetworkTransformStateManager.InterpolatorTarget.Rotation, value);
26422642
}
26432643
else
26442644
{
@@ -2670,7 +2670,7 @@ private void UpdateScaleInterpolator(Vector3 scale, double time, bool resetInter
26702670
var value = new float4(scale.x, scale.y, scale.z, 0.0f);
26712671
if (resetInterpolator)
26722672
{
2673-
m_CachedNetworkManager.TransformStateManager.ResetTo(InterpolatorIndex, NetworkTransformStateManager.InterpolatorTarget.Scale, value, time);
2673+
m_CachedNetworkManager.TransformStateManager.ResetTo(InterpolatorIndex, NetworkTransformStateManager.InterpolatorTarget.Scale, value);
26742674
}
26752675
else
26762676
{

com.unity.netcode.gameobjects/Runtime/Components/NetworkTransformDeltaCheck.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ public partial class NetworkTransform
1111
/// Everything <see cref="CheckForStateChange"/> needs from the instance it is checking.
1212
/// </summary>
1313
/// <remarks>
14-
/// This creates the abstraction layer between <see cref="NetworkTransform"/> itself and the configuration
15-
/// of the <see cref="NetworkTransform"/> in order to assure the delta check can be run both on the main
16-
/// thread and from within a job.<br />
14+
/// Separates the delta check from <see cref="NetworkTransform"/> itself, which is what lets it run both
15+
/// on the main thread and from within a job.<br />
1716
/// A few members are read/write: the check can change the transform space it operates in and it
1817
/// advances the axial frame synchronization bookkeeping, both of which have to make it back to the
1918
/// instance.
@@ -54,7 +53,8 @@ internal struct TransformDeltaConfig
5453
internal int HalfFloatTargetTickOwnership;
5554

5655
/// <summary>
57-
/// Read/write. The tick slot this instance next sends an axial frame synchronization on.
56+
/// Read/write. The next tick to send an axial frame synchronization, if a delta has been sent
57+
/// since the last one.
5858
/// </summary>
5959
internal int NextTickSync;
6060

@@ -156,12 +156,12 @@ internal struct TransformDeltaEntry
156156

157157
/// <summary>
158158
/// Abstraction Layer Method:
159-
/// Resolves which transform space the delta check operates in.
159+
/// Resolves the transform space for both per-instance and batched mode delta checks.
160160
/// </summary>
161161
/// <remarks>
162162
/// Runs before the transform is sampled because it determines whether the local or the world values
163163
/// are the ones being compared. Kept separate (as opposed to being folded into
164-
/// <see cref="CheckForStateChange"/>) so that neither caller has to sample both spaces.
164+
/// <see cref="CheckForStateChange"/>) so that neither caller has to sample both transform spaces.
165165
/// </remarks>
166166
/// <param name="config">The instance configuration. <see cref="TransformDeltaConfig.InLocalSpace"/> may be updated.</param>
167167
/// <param name="flagStates">The state flags being updated.</param>

0 commit comments

Comments
 (0)