Visitar URL original
fix(vec): round half-way cases away from zero in the SIMD backends by MaxFreedomPollard · Pull Request #831 · bitshifter/glam-rs · GitHub
Skip to content

fix(vec): round half-way cases away from zero in the SIMD backends - #831

Merged
bitshifter merged 1 commit into
bitshifter:mainfrom
MaxFreedomPollard:fix-simd-round-half-away-from-zero
Sep 7, 2026
Merged

bitshifter merged 1 commit into
bitshifter:mainfrom
MaxFreedomPollard:fix-simd-round-half-away-from-zero

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

Objective

  • Vec3A::round and Vec4::round are documented to round half-way cases away from zero, which is what f32::round and the scalar backend do, but the SSE2, NEON and wasm backends round them to the nearest even integer. On aarch64 Vec3A::new(0.5, -0.5, 2.5).round() is (0.0, -0.0, 2.0) while the scalar Vec3::new(0.5, -0.5, 2.5).round() is (1.0, -1.0, 3.0), so the answer depends on the target.

  • One line per backend causes it. templates/vec.rs.tera line 2020 emits vrndnq_f32 for NEON, which is FRINTN, round to nearest with ties to even, and line 2016 emits f32x4_nearest for wasm, which is ties to even as well. On SSE2, m128_round in src/sse2.rs line 148 is DirectXMath's XMVectorRound, which rounds by adding and subtracting 8388608.0 and therefore follows the current rounding mode, ties to even again. The existing test_round cases miss it because 1.5 and -15.5 round the same way under both rules.

Solution

  • NEON uses vrndaq_f32 (FRINTA), the single instruction that has exactly the documented behaviour, so it is not slower.

  • SSE2 keeps the DirectXMath helper under the name m128_round_ties_even, which is still what m128_mod_angles wants, and m128_round corrects it. The two rules differ only on half-way cases that were rounded towards zero, and those are exactly the lanes where the input minus the rounded value is 0.5 with the sign of the input, so those lanes get one more in the direction of the input. That is a compare and two bitwise ops on top of the existing sequence.

  • wasm gets the same correction on top of f32x4_nearest in a new f32x4_round in src/wasm.rs. It selects with v128_bitselect rather than adding zero to the untouched lanes, so -0.0 still rounds to -0.0.

  • The core-simd backend already rounds half-way cases away from zero, so it is unchanged.

Code generation

  • Yes. templates/vec.rs.tera was edited and cargo run --release -p codegen was run. The generated part of the diff is one line in each of src/f32/neon/vec3a.rs, src/f32/neon/vec4.rs, src/f32/wasm/vec3a.rs and src/f32/wasm/vec4.rs. src/sse2.rs and src/wasm.rs are hand written. cargo run -p ci -- codegen passes.

Testing and linting

  • The half-way cases are added to the existing test_round in tests/vec2.rs, tests/vec3.rs and tests/vec4.rs. Without the fix, vec3a::test_round fails on aarch64 with left: Vec3A(0.0, -0.0, 2.0) against right: Vec3A(1.0, -1.0, 3.0) and vec4::test_round fails on both aarch64 and x86_64. With the fix all three test files pass on aarch64, on x86_64 and with scalar-math.

  • I also ran a throwaway test comparing Vec4::splat(v).round() and Vec3A::splat(v).round() against v.round() for every 65536th f32 bit pattern from 0 up to 2^24 in both signs, plus the largest float below 0.5, 2^23, the infinities and NaN. With the change it passes on the scalar, SSE2, NEON, core-simd and wasm backends, and without it the wasm one aborts.

  • cargo run -p ci -- test-features passes all nine feature sets, and cargo fmt --all -- --check and cargo clippy -p glam --all-targets are clean. I checked wasm by building the tests for wasm32-wasip1 with -C target-feature=+simd128 and running them under node, because the wasm_bindgen_test harness in tests/ needs a browser.

Vec3A::round and Vec4::round are documented to round half-way cases away
from zero, which is what f32::round and the scalar backend do, but the
SSE2, NEON and wasm backends round them to the nearest even integer. On
aarch64 Vec3A::new(0.5, -0.5, 2.5).round() is (0.0, -0.0, 2.0) where the
scalar Vec3::new(0.5, -0.5, 2.5).round() is (1.0, -1.0, 3.0), so the
answer depends on the target.

templates/vec.rs.tera emitted vrndnq_f32 (FRINTN, ties to even) for NEON
at line 2020 and f32x4_nearest (also ties to even) for wasm at line 2016.
m128_round in src/sse2.rs line 148 is DirectXMath's XMVectorRound, which
rounds by adding and subtracting 8388608.0 and so follows the current
rounding mode, ties to even again. The existing test_round cases miss it
because 1.5 and -15.5 round the same way under both rules.

NEON now uses vrndaq_f32 (FRINTA), the single instruction with the
documented behaviour. The SSE2 helper keeps its old behaviour under the
name m128_round_ties_even, which is still what m128_mod_angles wants, and
m128_round corrects it: the two rules differ only on half-way cases that
were rounded towards zero, and those are the lanes where the input minus
the rounded value is 0.5 with the sign of the input, so those lanes get
one more in the direction of the input. src/wasm.rs gets f32x4_round
doing the same correction on top of f32x4_nearest, selecting with
v128_bitselect so -0.0 still rounds to -0.0. The core-simd backend
already rounded away from zero and is unchanged.

Half-way cases are added to the existing test_round in tests/vec2.rs,
tests/vec3.rs and tests/vec4.rs.
@bitshifter bitshifter added the bug Something isn't working label Sep 6, 2026
@bitshifter
bitshifter merged commit 40535a3 into bitshifter:main Sep 7, 2026
31 checks passed
@github-actions github-actions Bot mentioned this pull request Sep 6, 2026
bitshifter added a commit that referenced this pull request Sep 7, 2026
## 🤖 New release

* `glam`: 0.33.6 -> 0.33.7 (✓ API compatible changes)

<details><summary><i><b>Changelog</b></i></summary><p>

<blockquote>

##
[0.33.7](0.33.6...0.33.7)
- 2026-09-07

### Fixed

- *(vec)* round half-way cases away from zero in the SIMD backends
([#831](#831))

### Other

- add neon float32x4_t sin implementation.
([#834](#834))
- further optimisations to to_scale_rotation_translation
([#829](#829))
- *(mat4)* specialize affine determinant in SRT decomposition
([#827](#827))
- *(quat)* avoid acos in is_near_identity
([#824](#824))
- Use m128_sin in Vec3A::slerp
([#819](#819))
</blockquote>


</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cameron Hart <cameron.hart@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants