Skip to content

fix: half float delta position dither - #4128

Open
NoelStephensUnity wants to merge 4 commits into
develop-2.0.0from
fix/half-float-delta-position-dither
Open

fix: half float delta position dither#4128
NoelStephensUnity wants to merge 4 commits into
develop-2.0.0from
fix/half-float-delta-position-dither

Conversation

@NoelStephensUnity

@NoelStephensUnity NoelStephensUnity commented Aug 18, 2026

Copy link
Copy Markdown
Member

Purpose of this PR

NetworkTransform.UseHalfFloatPrecision made objects appear to jitter on non-authority instances while they were stationary or coming to rest, even though the authority was not moving them.

NetworkDeltaPosition encodes position as a half float delta from a base position, and carries the rounding loss of each update into the next one. That keeps the average transmitted position accurate while a value is moving. The problem is that the rounding loss alone is enough to change the encoded delta, so once the value stops moving that mechanism keeps changing what gets sent even though the position has not changed. The encoded value alternates between neighbouring representable values, and a stationary object is transmitted as one that oscillates.

The rounding loss is now only carried forward while the value moves by at least one representable step between updates. Below that the delta is sent as-is, which still follows slow movement but cannot introduce movement of its own. The threshold comes from the encoding rather than a configured value, so it engages where the resolution is coarse enough to matter and stays out of the way near the base position.

MaxDeltaBeforeAdjustment was a second, quieter half of the same problem: it determined the transmitted resolution, because a half float's step size grows with its magnitude. At 64 the coarsest step was 31.25mm, so objects away from their base position were reproduced in ~3cm increments. At 2 it is 0.977mm, which is finer than the default PositionThreshold of 1mm. The two settings were previously incoherent with each other.

Tradeoffs worth reviewer attention:

  • Folding the delta into the base more often costs no bandwidth with reliable deltas, since both sides apply the same rule to the same value and nothing is transmitted to keep them in agreement. UseUnreliableDeltas forces a full precision base synchronization on each fold, so those projects will send more of them. This is the only behavioural cost.
  • Sender and receiver must agree on MaxDeltaBeforeAdjustment, so the change is not compatible across builds. This is already enforced, since NetworkConstants.PROTOCOL_VERSION participates in the connection config hash and mismatched versions cannot connect.
  • Suppressing the rounding loss slightly reduces average-position accuracy for objects that are barely moving, in exchange for not inventing movement. Measurements below show accuracy improved in every case tested, including objects in motion.

Jira ticket

Changelog

com.unity.netcode.gameobjects

  • Fixed: Objects using NetworkTransform.UseHalfFloatPrecision appeared to jitter on non-authority instances while they were stationary or coming to rest, even though the authority was not moving them.
  • Changed: NetworkTransform.UseHalfFloatPrecision now synchronizes position with a resolution of approximately 1mm regardless of how far an object has travelled. Previously the resolution could degrade to approximately 3cm. This does not increase bandwidth, but projects using NetworkTransform.UseUnreliableDeltas will send full precision position updates more often.

Documentation

  • No documentation changes or additions were necessary. No public API was added or changed; the new helper and the adjusted constant are both internal.

One gap was left out of scope: the relationship between UseHalfFloatPrecision and PositionThreshold is still undocumented. Setting a threshold finer than the encoding can represent is not meaningful. That is much less likely to matter at ~1mm resolution, but it is worth a follow-up doc note.

Testing & QA (How your changes can be verified during release Playtest)

Reproduced with the physics ball stress test in the NGO Examples project: 10 balls spawned, allowed to come to rest, with the client observed and recorded. The jitter appears on resting objects only once they are some distance from where they spawned, which is why it can look intermittent.

Measured on 10 settling objects with half float enabled throughout, showing per-update position change and direction reversals on the client:

movement per update direction reversals
before 28-42mm 22-39
after 3.1-6.0mm 0
reference: half float disabled 2.7-5.6mm 0

After the change, half float precision is indistinguishable from float precision in this scene. The authority was confirmed stationary throughout, so the movement was being introduced between the authority and the client rather than reproduced from it. Objects in motion also improved, with peak error dropping from 12.5mm to 0.587mm.

Functional Testing

Manual testing :

  • Manual testing done - NGO Examples physics ball stress test, standalone IL2CPP client against an editor host, comparing before and after builds.

Automated tests:

  • Covered by existing automated tests - the full testproject and runtime test runners pass. Note that these did not previously cover this behaviour.
  • Covered by new automated tests - NetworkTransformHalfFloatPrecisionTests, two cases across Host and DAHost.

The new tests were verified in both directions. Without the fix all four cases fail on the intended assertion, reporting 7.9mm to 10.1mm of backwards movement. With the fix all four pass.

Does the change require QA team to:

  • Review automated tests? - the tests assert that the client never moves opposite to the authority rather than asserting a position tolerance. Interpolation cannot overshoot, so backwards movement can only have come from the encoding, and the assertion does not need revisiting when the resolution changes. They also depend on two setup details that look arbitrary but are not, both commented: the object has to travel away from its spawn position, and it then has to step by an amount the encoding cannot represent before coming to rest. A position that a half float represents exactly leaves no rounding loss and so cannot exhibit the problem.
  • Execute manual tests? - specifically a project using UseUnreliableDeltas together with UseHalfFloatPrecision, which is the one configuration that sends more data after this change.
  • Provide feedback about the PR?

These tests do not use the time travel harness, since the behaviour only appears over multiple real state update and interpolation cycles. Runtime is roughly 3.5s per case.

Up-port

#4129 is the up-port.
Required. NetworkDeltaPosition.cs is identical on develop-3.x.x, and the test folder and base classes are the same, so both commits should apply cleanly.

Backports

Not needed. NetworkDeltaPosition does not exist on develop (v1.x) and half float position synchronization is not present there.

NoelStephensUnity and others added 2 commits August 18, 2026 15:18
NetworkDeltaPosition carries the half float rounding loss of each update
into the next one, which keeps the average transmitted position accurate
while a value is moving. The loss alone is enough to change the encoded
delta, so once the value stops moving that mechanism keeps changing what is
sent even though the position has not moved. The encoded value alternates
between neighbouring representable values and a stationary object is
transmitted as one that oscillates. The rounding loss is now only carried
forward while the value moves by at least one representable step.

MaxDeltaBeforeAdjustment also determined the transmitted resolution, since
a half float's step size grows with its magnitude. At 64 the coarsest step
was 31.25mm, so objects away from their base position were reproduced in
~3cm increments. At 2 it is 0.977mm. Folding the delta into the base more
often costs no bandwidth with reliable deltas because both sides apply the
same rule to the same value, and the reconstructed position is unchanged by
the fold. UseUnreliableDeltas forces a full precision base synchronization
per fold, so those projects will send those more often.

Measured on 10 settling physics objects with half float enabled: 28-42mm of
oscillation before, none after, matching the same scene with half float
disabled. Objects in motion improve as well, peak error dropping from
12.5mm to 0.587mm.

Sender and receiver must agree on MaxDeltaBeforeAdjustment, so this is not
compatible across builds. NetworkConstants.PROTOCOL_VERSION already
participates in the connection config hash, so mismatched versions cannot
connect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two NetcodeIntegrationTest cases, one for an object moving in steps too
small for the encoding to represent and one for an object at rest. Both
move the authority forwards only and require non-authority instances to
follow without ever moving backwards. Interpolation cannot overshoot, so
movement opposite to the authority's has to have come from the encoding.
That also avoids a tolerance that would need revisiting whenever the
resolution changes.

Two setup details are needed for these to detect anything. The object has to
travel away from the base position established when it spawned, since
resolution is fine near the base. It then has to step by an amount the
encoding cannot represent before coming to rest, because a position a half
float represents exactly leaves no rounding loss and so cannot exhibit the
problem: resting on 30.0 produces no backwards movement at all while resting
on 30.0007 produces 15.6mm.

Verified in both directions. Without the fix all four cases fail on the
intended assertion, reporting 7.9mm to 10.1mm of backwards movement. With
the fix all four pass.

These do not use the time travel harness because the behavior only appears
over multiple real state update and interpolation cycles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@NoelStephensUnity NoelStephensUnity changed the title Fix/half float delta position dither fix: half float delta position dither Aug 18, 2026
@svc-netcode-sdk
svc-netcode-sdk requested a review from a team August 18, 2026 20:26
Updated changelog entries for NetworkTransform.UseHalfFloatPrecision to reflect changes in issue tracking numbers.
@codecov-github-com

codecov-github-com Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 86.66667% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...objects/Runtime/Components/NetworkDeltaPosition.cs 86.66% 2 Missing ⚠️
@@                Coverage Diff                @@
##           develop-2.0.0    #4128      +/-   ##
=================================================
+ Coverage          73.86%   73.99%   +0.12%     
=================================================
  Files                172      172              
  Lines              28099    28112      +13     
=================================================
+ Hits               20756    20801      +45     
+ Misses              7343     7311      -32     
Flag Coverage Δ
NGOv2_project_testproject_ubuntu_pinnedTrunk 73.69% <86.66%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...objects/Runtime/Components/NetworkDeltaPosition.cs 83.49% <86.66%> (+12.38%) ⬆️

... and 1 file with indirect coverage changes

Components Coverage Δ
com.unity.netcode.gameobjects 73.99% <86.66%> (+0.12%) ⬆️

ℹ️ Need help interpreting these results?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant