Sim/COB: avoid UB in float-to-short angle casts (fixes arm64/x86 desync)#3075
Open
tomjn wants to merge 2 commits into
Open
Sim/COB: avoid UB in float-to-short angle casts (fixes arm64/x86 desync)#3075tomjn wants to merge 2 commits into
tomjn wants to merge 2 commits into
Conversation
Casting a floating-point heading/pitch expression directly to `short` is undefined behaviour when the truncated value does not fit in `short`, and this is genuinely reached: `heading * RAD2TAANG` equals +-32768 at heading = +-pi, one past short's 32767 maximum. Because the result is UB, arm64 and x86 produced different values at the angle extremes, which was observed as an arm64/x86 multiplayer desync. x86 already lowers `short(float)` as float->int->short (cvttss2si into a 32-bit register, then narrow), so making the `int()` step explicit leaves the already-correct x86 result unchanged while pinning arm64 (whose fcvtzs saturated differently) to the same value. The int->short narrowing is the intended 16-bit TA-angle wraparound. Sites fixed in CobInstance.cpp: WindChanged, StartBuilding, AimWeapon. Sibling casts were checked and left untouched: UnitScript.cpp:1058 casts asin(...)*RAD2TAANG (range +-16384, always fits short, no UB) and lines 1095/1099 already route through int; CobThread.cpp has no such casts. Pure correctness fix to synced simulation code; it does not alter results on the platform that was already correct. Cross-arch sync validation is recommended. Origin: discussed in PR beyond-all-reason#2991's review (credit BambaDamba). AI assistance: implemented with Claude Code (Anthropic) from a written plan; reasoning and build verification reviewed by a human.
Collaborator
|
wtf float to short can be UB?? absolutely nutty |
Collaborator
|
This sounds like something that requires a comment. Perhaps add some sort of |
Contributor
|
Floating point to integral conversions can be UB if the truncated value is not representable in the destination type. It would be better IMO to limit the range on this value before converting to short and not do the float->int->short conversion because that is extremely confusing. |
…mment sprunk asked for a comment and a named helper around the float->short angle cast. Wrap the conversion in RadAngleToCobShort() and document why it exists: COB angles are circular 16-bit TA units (full turn == COBSCALE), so values past a half turn intentionally wrap modulo 2^16 via the int->short narrowing. The float->int step stays because the wrap is the desired behaviour and is deterministic across arm64/x86; clamping the range (as suggested) would break angles past a half turn rather than wrapping them. No behavioural change vs the previous short(int(...)) form.
sprunk
approved these changes
Jun 30, 2026
sprunk
left a comment
Collaborator
There was a problem hiding this comment.
I sort of agree with n-morales that it would be good to come up with something less confusing than daisy chaining static casts, but since a comment exists I'm not too unhappy about it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Casting a floating-point heading/pitch expression directly to
shortis undefined behaviour when the truncated value does not fit inshort, and this is genuinely reached in the COB angle math:RAD2TAANG = COBSCALE_HALF / piwithCOBSCALE_HALF = 32768, soheading * RAD2TAANGequals+-32768atheading = +-pi— one pastshort's32767maximum.This routes the conversion through
intfirst (short(int(expr))) at every affected site, which is well-defined:float -> inttruncates toward zero (the magnitudes here always fitint), thenint -> shortperforms the intended 16-bit TA-angle wraparound.Why it matters (determinism)
Because the direct
float -> shortis UB, arm64 and x86 produced different values at the angle extremes, which was observed as an arm64/x86 multiplayer desync.This is synced simulation code, so the fix must not change results on the platform that was already correct. It doesn't: x86 already lowers
short(float)asfloat -> int -> short(cvttss2siinto a 32-bit register, then narrow), so making theint()step explicit leaves the x86 result unchanged while pinning arm64 (whosefcvtzssaturated differently) to that same value.Sites
Fixed in
rts/Sim/Units/Scripts/CobInstance.cpp:WindChanged(SetDirection)StartBuilding(heading + pitch)AimWeapon(heading + pitch)Siblings checked and deliberately left untouched (not the same bug):
UnitScript.cpp:1058castsasin(...) * RAD2TAANG— range+-16384, always fitsshort, no UB.UnitScript.cpp:1095/1099already route throughint.CobThread.cpphas no such casts.Testing
engine-headlesscleanly (CobInstance compiles into the sim).int()step reproduces x86's existing lowering, so the already-correct platform is unaffected.Origin / credit
The fix was discussed in PR #2991's review thread (credit to BambaDamba); it was never committed there. Implemented here from that description and verified independently.
AI disclosure
Implemented with Claude Code (Anthropic) from a written plan. Reasoning, scope decisions, and the build verification were reviewed by a human (per
AI_POLICY.md).