Commit 95ba207
fix: half float delta position dither (#4128)
* fix: half float position encoding manufactures motion on resting objects
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>
* test: integration coverage for half float position encoding
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>
* Update CHANGELOG for NetworkTransform precision changes
Updated changelog entries for NetworkTransform.UseHalfFloatPrecision to reflect changes in issue tracking numbers.
* test - update
Adding better coverage and adjusting some of the test to better leverage from NetcodeIntegrationTest helper methods.
* test - update
Moving the NetworkDeltaPositionTests into its own file.
* update
Bumping the protocol version to assure legacy clients cannot connect to a session with the fixes. While the fixes aren't technically a "breaking change", any projects using the legacy lerp will end up with an offset from the expected final position. Since motion continually feeds the full position (half float or full precision) as deltas this would prevent from "long term drift".
Either case, updating the protocol version only assures that clients of a previous version cannot connect to a session with the newer version.
* test: Refer to the single non-authority instance directly
There is one connected client, so the per instance dictionaries and the
loops over m_NetworkManagers only ever held one entry. Resolve the
non-authority instance once through GetNonAuthorityNetworkManager and
name the two sampling frame counts that were inline literals.
* update
Removed note about why we don't use time travel (too verbose).
Enabling this on the CMB service pass to validate this works as expected against a live session.
* test: Drop two NetworkDeltaPosition tests that cover an already covered path
ADeltaUnderTheThresholdIsLeftAsADelta asserted nothing the other tests do
not: MovingFoldsThePreviousRoundingLossBackIn already requires the delta
to stay under the threshold, and UnsynchronizedAxesAreLeftUntouched
already requires the synchronized axis to hold the movement.
QuantumDropsTheSignBecauseTheLatticeIsSymmetric ran its own value list to
assert one thing, so it moves into the loop in
QuantumIsTheSmallestChangeTheEncodingCanSee, which already walks the same
kind of values. 300f joins that list so no input is lost.
The masking it covers is a common path executed by every call, so the
coverage score is unchanged either way.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>1 parent 5493a5d commit 95ba207
7 files changed
Lines changed: 665 additions & 4 deletions
File tree
- com.unity.netcode.gameobjects
- Runtime
- Components
- Configuration
- Tests
- Editor
- Runtime/NetworkTransform
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
| 17 | + | |
16 | 18 | | |
17 | 19 | | |
18 | 20 | | |
| |||
27 | 29 | | |
28 | 30 | | |
29 | 31 | | |
| 32 | + | |
30 | 33 | | |
31 | 34 | | |
32 | 35 | | |
| |||
Lines changed: 45 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
14 | | - | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
15 | 22 | | |
16 | 23 | | |
17 | 24 | | |
| |||
138 | 145 | | |
139 | 146 | | |
140 | 147 | | |
141 | | - | |
142 | 148 | | |
143 | 149 | | |
144 | 150 | | |
145 | 151 | | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
146 | 162 | | |
147 | 163 | | |
148 | | - | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
149 | 171 | | |
150 | 172 | | |
151 | 173 | | |
| |||
165 | 187 | | |
166 | 188 | | |
167 | 189 | | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
168 | 210 | | |
169 | 211 | | |
170 | 212 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
| 8 | + | |
9 | 9 | | |
10 | 10 | | |
0 commit comments