mirror of
https://github.com/opencv/opencv.git
synced 2026-10-02 04:02:40 +03:00
core: saturating float/double->intXY conversions in cvRound/cvFloor/cvCeil and the corresponding univ. intrinsics. - #30007 Merge pull request #30007 from vpisarev:fix_flt2int Fixes #28557 (supersedes the tail-only fix from #29895). ### The problem On x86 `cvtss2si`/`cvtsd2si`/`cvtps2dq` return the "integer indefinite" value `0x80000000` (`INT_MIN`) for any out-of-range input, so ```cpp cvRound(3e9); // INT_MIN saturate_cast<ushort>(60000.f*60000.f); // 0 instead of 65535 (#28557: the scalar tail of cv::multiply) v_round(v_float32(1e10f)); // INT_MIN lanes ``` The problem is not x86-only. `cvRound()` on aarch64, riscv64 and loongarch64 went through `(int)lrint()`, which the compilers implement as a 64-bit conversion followed by truncation to 32 bits, i.e. large values *wrap* instead of saturating (`fcvtzs x0` + `mov w0`, `fcvt.l.d` + `sext.w`). NEON `v_round/v_floor/v_ceil/v_trunc(v_float64x2)` narrowed with a wrapping `vmovn`, and `saturate_cast<unsigned/int64/uint64>(float/double)` were plain UB above the target range. ### The fix **`fast_math.hpp`** - `cvRound()`, `cvFloor()`, `cvCeil()` saturate on every platform. New `cvTrunc()` (round towards zero, saturating; the scalar counterpart of `v_trunc`) and `cvRound64()` (round-half-to-even to `int64`, saturating). - x86 (SSE2): the input is clamped from above (`min`) before `cvt*`; the "indefinite" value is already the correct result below `INT_MIN`. `cvFloor` also clamps from below because of the "-1" correction. - aarch64 (`__GNUC__`/clang): one-instruction inline asm `fcvtns/fcvtms/fcvtps/fcvtzs` (GCC's `arm_neon.h` has no scalar f64→s32 ACLE functions, and inline asm needs no header). - riscv64: `fcvt.w.{s,d}` / `fcvt.l.{s,d}` with an explicit rounding mode (`rne`, `rdn`, `rup`, `rtz`). - loongarch64: `ftint*.w.{s,d}` + `movfr2gr.s` (the old `.l.d` + `movfr2gr.d` wrapped); `cvRound` got a branch too. - MSVC ARM64: the 64-bit results are clamped before narrowing. - Portable `#else` branch: hand-written clamp before the conversion, no new headers. - Exact semantics: `double` saturates to `INT_MIN`/`INT_MAX` exactly. `INT_MAX` is not representable as `float`, so `float` inputs `>= 2^31` give **2147483520** (the largest float below 2^31, the clamp value) where the instruction does not saturate by itself (x86, portable) and `INT_MAX` where it does (ARM, RISC-V, LoongArch). Both are accepted by the tests and documented. - **NaN handling is out of scope in this PR**. **`saturate.hpp`** - `float/double → unsigned/int64/uint64` now saturate and use round-half-to-even via `cvRound64()` (no libm `round()` call; OpenCV builds without `-ffast-math`, where `rint()`/`round()` are real calls). - `int/int64 → schar/short/int` range checks are done in unsigned arithmetic. The old `(unsigned)(v - SHRT_MIN)` overflowed (UB) for `v > INT_MAX - 32768`; such values were unreachable before but are returned by the saturating `cvRound()` now, and GCC 15 actually exploits the UB (derives `v <= INT_MAX-32768` and folds neighbouring comparisons). **Universal intrinsics** - SSE, AVX2, AVX-512: clamp before `cvt` in `v_round/v_floor/v_ceil/v_trunc` (f32 and f64, `v_round(a, b)` included). AVX2 keeps `_mm256_floor_ps/_ceil_ps`. - NEON f64: saturating narrow (`vqmovn_s64`); `v_floor/v_ceil` use `fcvtms/fcvtps` directly; `v_trunc` used `vcvtaq` (round-to-nearest-away) instead of truncation. - WASM f32: clamp before the `-1/+1` corrections (they wrapped `INT_MIN`); `v_round` was `trunc_sat(a + 0.5)` (wrong for negatives and ties), now `f32x4.nearest`; f64 `v_trunc` via `cvTrunc`. - MSA f64: clamp before `pckev.w`, which takes the low 32 bits of the saturated int64. - RVV f16 `v_floor`: rounding mode `2` (RDN) instead of `1` (RTZ). - VSX, LSX/LASX, RVV f32/f64, RVV 0.7.1: untouched, the conversion instructions saturate by ISA definition. - `intrin_cpp.hpp`: `v_trunc` via `cvTrunc`; docs mention the saturation. `arithm.simd.hpp` is not modified: `Core_Arithm.mul_overflow_28557` passes because the scalar tail's `saturate_cast<ushort>(float)` saturates now. ### Tests - `Core_Arithm.mul_overflow_28557` re-enabled. - `Core_Arithm.mul_overflow_tail_and_inplace`: 8U/8S/16U/16S, row lengths 1..70 (vector body + scalar tail), out-of-place and in-place `multiply`, `mul`, `pow(x, 2)`. - `Core_ConvertTo.float_overflow_saturation`: 32F/64F → 8U/8S/16U/16S/32S/32U/64S/64U with `±1e10`, `±1e30`, `±inf`, `2147483647.5`, `x.5` etc.; vector body vs. scalar reference, extreme inputs must hit the type limits. - `Core_FastMath.SaturatingRoundingOps`: boundary table (`2147483647.5`, `-2147483648.5`, `2147483520f`, `-2147483904f`, `±DBL_MAX`, `±inf`, ...) plus a 200k-value log-uniform random sweep against `nearbyint/floor/ceil/trunc` clamped to `int`. - `Core_FastMath.Round64`: boundaries around `±2^63`, `.5` cases, random sweep. - `Core_SaturateCast.FloatToIntSaturation`, `RoundHalfToEven`, `IntToNarrowerIntBoundaries` (regression for the signed-overflow UB). - `test_intrin_utils.hpp` (`test_float_math`, `test_round_pair_f64`): overflow/boundary lanes for f32, f64 and f16, compared against the scalar functions and checked against explicit `INT_MIN` / `[2147483520, INT_MAX]` / `INT_MAX` limits. Runs on every backend and dispatch level. ### Verification - x86-64, GCC 15.2, `CPU_BASELINE=SSE4_2 CPU_DISPATCH=AVX2,AVX512_SKX`: full `opencv_test_core` passes (16864 tests), i.e. SSE4.2 baseline, AVX2 dispatch and the CPP emulator paths. AVX-512 is compile-checked only (no AVX-512 hardware here). - The standalone scalar checks also pass with the portable `#else` branch forced (`-U__SSE2__`) and under `-fsanitize=undefined`. - aarch64 (`aarch64-linux-gnu-gcc-14`) and riscv64 (`riscv64-linux-gnu-gcc-14 -march=rv64gcv_zvfh`): compile-checked, generated code inspected (`fcvtns w0, d0`, `fcvt.w.d a0, fa0, rne`, `sqxtn`, `vfcvt.x.f.v`, ...). Not run (no hardware/qemu). - **Not verified at all** (please watch CI): LoongArch (`ftint*.w.d`/`ftintrne.*` asm), WASM (`wasm_f32x4_nearest`), MSA, MSVC ARM64 (`vcvtd_s64_f64`, `vcvts_s32_f32`, `vcvtnd_s64_f64`). ### Behaviour changes worth noting - `saturate_cast<unsigned/int64/uint64>(x.5)` now rounds half to even (was half away from zero), consistent with all the other integer targets. - `cvRound(float)` for inputs `>= 2^31` returns **2147483520** on x86 (was `INT_MIN`); `INT_MAX` on ARM/RISC-V. It would be noticeably slower to implement true saturation to `INT_MAX`. Note that around `INT_MAX` float's cannot represent the integer's exactly anyway. - `saturate_cast<int>(1e10)` is `INT_MAX` (documentation used to say "no clipping is done for 32-bit integers"). ### Pull Request Readiness Checklist - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on code under GPL or another license incompatible with OpenCV. - [x] The PR is proposed to the proper branch (`5.x`). - [x] There is a reference to the original bug report and related work: #28557, #29895. - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable: accuracy tests added, no test data needed. - [x] The feature is well documented and sample code can be built with the project CMake: doxygen comments of `cvRound`/`cvFloor`/`cvCeil`/`cvTrunc`/`cvRound64`, `saturate_cast` and the intrinsics conversion group updated. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01KVnDjvrFtxcyw7RFzSPews