diff --git a/core/benches/conversions.rs b/core/benches/conversions.rs index ef56302..489249b 100644 --- a/core/benches/conversions.rs +++ b/core/benches/conversions.rs @@ -196,6 +196,24 @@ fn bench_f64_to_i32(c: &mut Criterion) { .unwrap(); }); }); + + // No out_of_range (error on overflow) — all values in range + let config_none = FloatToIntConfig { + map_entries: vec![], + rounding: RoundingMode::NearestEven, + out_of_range: None, + }; + group.bench_with_input(BenchmarkId::new("f64_to_i32/no_oor", n), &n, |b, &n| { + let mut dst = vec![0i32; n]; + b.iter(|| { + convert_slice_float_to_int( + black_box(&src), + black_box(&mut dst), + black_box(&config_none), + ) + .unwrap(); + }); + }); } group.finish(); } @@ -315,6 +333,28 @@ fn bench_f64_to_f32(c: &mut Criterion) { .unwrap(); }); }); + + // Towards-zero rounding, no out_of_range (scalar fallback path) + let config_tz = FloatToFloatConfig { + map_entries: vec![], + rounding: RoundingMode::TowardsZero, + out_of_range: None, + }; + group.bench_with_input( + BenchmarkId::new("f64_to_f32/towards_zero", n), + &n, + |b, &n| { + let mut dst = vec![0f32; n]; + b.iter(|| { + convert_slice_float_to_float( + black_box(&src), + black_box(&mut dst), + black_box(&config_tz), + ) + .unwrap(); + }); + }, + ); } group.finish(); } diff --git a/core/src/lib.rs b/core/src/lib.rs index 48b4a36..b294cec 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -635,6 +635,27 @@ where } } + // SIMD fast path: empty scalar_map + no out_of_range (error mode) + supported rounding. + // Range-checking variant: errors if any value is out of range. + if config.map_entries.is_empty() + && config.out_of_range.is_none() + && config.rounding != RoundingMode::NearestAway + { + use std::any::TypeId; + + // f64 → i32 + if TypeId::of::() == TypeId::of::() && TypeId::of::() == TypeId::of::() + { + let src_f64: &[f64] = + unsafe { std::slice::from_raw_parts(src.as_ptr() as *const f64, src.len()) }; + let dst_i32: &mut [i32] = + unsafe { std::slice::from_raw_parts_mut(dst.as_mut_ptr() as *mut i32, dst.len()) }; + if simd::try_f64_to_i32_check(src_f64, dst_i32, config.rounding)? { + return Ok(()); + } + } + } + // Scalar fallback for (in_val, out_slot) in src.iter().zip(dst.iter_mut()) { *out_slot = convert_float_to_int(*in_val, config)?; @@ -659,15 +680,37 @@ where } /// Convert a slice of float values to float values. Returns early on first error. +/// +/// When the configuration allows it (empty scalar_map, nearest-even rounding), +/// uses SIMD-accelerated kernels for supported type pairs (f64->f32). pub fn convert_slice_float_to_float( src: &[Src], dst: &mut [Dst], config: &FloatToFloatConfig, ) -> Result<(), CastError> where - Src: CastFloat + CastInto, - Dst: CastFloat, + Src: CastFloat + CastInto + 'static, + Dst: CastFloat + 'static, { + // SIMD fast path: empty scalar_map + nearest-even rounding + f64→f32. + if config.map_entries.is_empty() && config.rounding == RoundingMode::NearestEven { + use std::any::TypeId; + + if TypeId::of::() == TypeId::of::() && TypeId::of::() == TypeId::of::() + { + // SAFETY: We just verified Src == f64 and Dst == f32 via TypeId. + let src_f64: &[f64] = + unsafe { std::slice::from_raw_parts(src.as_ptr() as *const f64, src.len()) }; + let dst_f32: &mut [f32] = + unsafe { std::slice::from_raw_parts_mut(dst.as_mut_ptr() as *mut f32, dst.len()) }; + let error_on_overflow = config.out_of_range != Some(OutOfRangeMode::Clamp); + if simd::try_f64_to_f32_nearest(src_f64, dst_f32, error_on_overflow)? { + return Ok(()); + } + } + } + + // Scalar fallback for (in_val, out_slot) in src.iter().zip(dst.iter_mut()) { *out_slot = convert_float_to_float(*in_val, config)?; } diff --git a/core/src/simd/aarch64.rs b/core/src/simd/aarch64.rs index f8ef07c..c068ac5 100644 --- a/core/src/simd/aarch64.rs +++ b/core/src/simd/aarch64.rs @@ -284,6 +284,157 @@ pub(super) unsafe fn f32_to_u8_clamp( Ok(()) } +/// Convert f64 slice to f32 slice using nearest-even rounding. +/// +/// NEON's `vcvt_f32_f64` performs the narrowing with nearest-even rounding +/// (the default IEEE 754 mode), processing 2 f64 → 2 f32 per instruction. +/// +/// Two-pass approach: +/// 1. Fast convert pass: just `vcvt_f32_f64` + store (no branching). +/// 2. If `error_on_overflow`, a second pass checks for finite→infinite overflow. +/// +/// This keeps the hot convert loop branch-free for maximum throughput. +/// +/// # Safety +/// +/// Caller must ensure this runs on an AArch64 target. +pub(super) unsafe fn f64_to_f32_nearest( + src: &[f64], + dst: &mut [f32], + error_on_overflow: bool, +) -> Result<(), crate::CastError> { + let n = src.len(); + let simd_len = n / 2 * 2; + + // Pass 1: branch-free narrowing conversion + for i in (0..simd_len).step_by(2) { + let v = vld1q_f64(src.as_ptr().add(i)); + let narrowed = vcvt_f32_f64(v); + vst1_f32(dst.as_mut_ptr().add(i), narrowed); + } + // Scalar tail + for i in simd_len..n { + dst[i] = src[i] as f32; + } + + // Pass 2: overflow check (only when out_of_range is None) + if error_on_overflow { + let inf_f32 = vdup_n_f32(f32::INFINITY); + for i in (0..simd_len).step_by(2) { + // Check if result is ±Inf + let result = vld1_f32(dst.as_ptr().add(i)); + let abs_result = vabs_f32(result); + let result_is_inf = vceq_f32(abs_result, inf_f32); + // Quick reject: if no Inf in result, no overflow possible + let inf_bytes: uint8x8_t = vreinterpret_u8_u32(result_is_inf); + if vmaxv_u8(inf_bytes) != 0 { + // At least one result is Inf — check if source was finite + for (&sv, &dv) in src[i..].iter().zip(dst[i..].iter()).take(2) { + if sv.is_finite() && dv.is_infinite() { + return Err(crate::CastError::OutOfRange { + value: sv, + lo: f32::MIN as f64, + hi: f32::MAX as f64, + }); + } + } + } + } + // Tail check + for i in simd_len..n { + if src[i].is_finite() && dst[i].is_infinite() { + return Err(crate::CastError::OutOfRange { + value: src[i], + lo: f32::MIN as f64, + hi: f32::MAX as f64, + }); + } + } + } + + Ok(()) +} + +/// Convert f64 slice to i32 slice with rounding, returning an error if any +/// value is out of range (no clamping). +/// +/// Same pipeline as `f64_to_i32_clamp` but instead of clamping, we +/// batch-check that all rounded values fall within [i32::MIN, i32::MAX] +/// and error if not. +/// +/// # Safety +/// +/// Caller must ensure this runs on an AArch64 target. +pub(super) unsafe fn f64_to_i32_check( + src: &[f64], + dst: &mut [i32], + rounding: RoundingMode, +) -> Result<(), crate::CastError> { + let n = src.len(); + let simd_len = n / 2 * 2; + + let lo = vdupq_n_f64(i32::MIN as f64); + let hi = vdupq_n_f64(i32::MAX as f64); + + for i in (0..simd_len).step_by(2) { + let v = vld1q_f64(src.as_ptr().add(i)); + + // NaN check + if any_nan_f64x2(v) { + for &val in &src[i..std::cmp::min(i + 2, n)] { + if val.is_nan() { + return Err(crate::CastError::NanOrInf { value: val }); + } + } + } + + let r = round_f64x2(v, rounding); + + // Range check: error if any value < lo or > hi + // vcltq_f64 returns all-ones for true, all-zeros for false + let below = vcltq_f64(r, lo); + let above = vcgtq_f64(r, hi); + let out_of_range = vorrq_u64(below, above); + let oor_bytes: uint8x16_t = vreinterpretq_u8_u64(out_of_range); + if vmaxvq_u8(oor_bytes) != 0 { + // Find exact offending element + for &val in src[i..].iter().take(2) { + let rounded = scalar_round_f64(val, rounding); + if rounded < i32::MIN as f64 || rounded > i32::MAX as f64 { + return Err(crate::CastError::OutOfRange { + value: val, + lo: i32::MIN as f64, + hi: i32::MAX as f64, + }); + } + } + } + + // Convert (values are in range, truncation after rounding is correct) + let i32_val = vmovn_s64(vcvtq_s64_f64(r)); + vst1_s32(dst.as_mut_ptr().add(i), i32_val); + } + + // Scalar tail + for i in simd_len..n { + let val = src[i]; + if val.is_nan() { + return Err(crate::CastError::NanOrInf { value: val }); + } + let rounded = scalar_round_f64(val, rounding); + if rounded < i32::MIN as f64 || rounded > i32::MAX as f64 { + return Err(crate::CastError::OutOfRange { + value: val, + lo: i32::MIN as f64, + hi: i32::MAX as f64, + }); + } + dst[i] = rounded as i32; + } + + Ok(()) +} + // --------------------------------------------------------------------------- // Scalar tail helpers (shared across kernels) // --------------------------------------------------------------------------- diff --git a/core/src/simd/avx.rs b/core/src/simd/avx.rs index edf8711..e074863 100644 --- a/core/src/simd/avx.rs +++ b/core/src/simd/avx.rs @@ -210,6 +210,134 @@ pub(super) unsafe fn f32_to_u8_clamp( Ok(()) } +/// Convert f64 slice to f32 slice using nearest-even (two-pass, AVX1). +/// +/// `_mm256_cvtpd_ps` is an AVX1 instruction: 4 f64 → 4 f32. +/// +/// # Safety +/// +/// Caller must ensure AVX is available. +#[target_feature(enable = "avx")] +pub(super) unsafe fn f64_to_f32_nearest( + src: &[f64], + dst: &mut [f32], + error_on_overflow: bool, +) -> Result<(), crate::CastError> { + let n = src.len(); + let simd_len = n / 4 * 4; + + // Pass 1: branch-free narrowing + for i in (0..simd_len).step_by(4) { + let v = _mm256_loadu_pd(src.as_ptr().add(i)); + let narrowed = _mm256_cvtpd_ps(v); + _mm_storeu_ps(dst.as_mut_ptr().add(i), narrowed); + } + for i in simd_len..n { + dst[i] = src[i] as f32; + } + + // Pass 2: overflow check + if error_on_overflow { + for i in 0..n { + if src[i].is_finite() && dst[i].is_infinite() { + return Err(crate::CastError::OutOfRange { + value: src[i], + lo: f32::MIN as f64, + hi: f32::MAX as f64, + }); + } + } + } + + Ok(()) +} + +/// Convert f64 slice to i32 slice with rounding, error on out-of-range (AVX1). +/// +/// # Safety +/// +/// Caller must ensure AVX is available. +#[target_feature(enable = "avx")] +pub(super) unsafe fn f64_to_i32_check( + src: &[f64], + dst: &mut [i32], + rounding: RoundingMode, +) -> Result<(), crate::CastError> { + let round_mode = avx_round_mode(rounding); + let n = src.len(); + let simd_len = n / 4 * 4; + + let lo = _mm256_set1_pd(i32::MIN as f64); + let hi = _mm256_set1_pd(i32::MAX as f64); + + for i in (0..simd_len).step_by(4) { + let v = _mm256_loadu_pd(src.as_ptr().add(i)); + + // NaN check + let nan_mask = _mm256_cmp_pd(v, v, _CMP_UNORD_Q); + if _mm256_movemask_pd(nan_mask) != 0 { + for &val in &src[i..std::cmp::min(i + 4, n)] { + if val.is_nan() { + return Err(crate::CastError::NanOrInf { value: val }); + } + } + } + + let r = round_f64_avx(v, round_mode); + + // Range check + let below = _mm256_cmp_pd(r, lo, _CMP_LT_OQ); + let above = _mm256_cmp_pd(r, hi, _CMP_GT_OQ); + let out_of_range = _mm256_or_pd(below, above); + if _mm256_movemask_pd(out_of_range) != 0 { + for &val in &src[i..std::cmp::min(i + 4, n)] { + let rounded = match rounding { + RoundingMode::NearestEven => val.round_ties_even(), + RoundingMode::TowardsZero => val.trunc(), + RoundingMode::TowardsPositive => val.ceil(), + RoundingMode::TowardsNegative => val.floor(), + RoundingMode::NearestAway => unreachable!(), + }; + if rounded < i32::MIN as f64 || rounded > i32::MAX as f64 { + return Err(crate::CastError::OutOfRange { + value: val, + lo: i32::MIN as f64, + hi: i32::MAX as f64, + }); + } + } + } + + let converted = _mm256_cvtpd_epi32(r); + _mm_storeu_si128(dst.as_mut_ptr().add(i) as *mut __m128i, converted); + } + + // Scalar tail + for i in simd_len..n { + let val = src[i]; + if val.is_nan() { + return Err(crate::CastError::NanOrInf { value: val }); + } + let rounded = match rounding { + RoundingMode::NearestEven => val.round_ties_even(), + RoundingMode::TowardsZero => val.trunc(), + RoundingMode::TowardsPositive => val.ceil(), + RoundingMode::TowardsNegative => val.floor(), + RoundingMode::NearestAway => unreachable!(), + }; + if rounded < i32::MIN as f64 || rounded > i32::MAX as f64 { + return Err(crate::CastError::OutOfRange { + value: val, + lo: i32::MIN as f64, + hi: i32::MAX as f64, + }); + } + dst[i] = rounded as i32; + } + + Ok(()) +} + // --------------------------------------------------------------------------- // Rounding helpers // --------------------------------------------------------------------------- diff --git a/core/src/simd/avx2.rs b/core/src/simd/avx2.rs index 4e3f5fc..0226493 100644 --- a/core/src/simd/avx2.rs +++ b/core/src/simd/avx2.rs @@ -263,6 +263,159 @@ pub(super) unsafe fn f32_to_u8_clamp( Ok(()) } +/// Convert f64 slice to f32 slice using nearest-even (two-pass). +/// +/// Pass 1: `_mm256_cvtpd_ps` narrows 4 f64 → 4 f32 (128-bit result). +/// Pass 2 (if `error_on_overflow`): scan for finite→infinite overflow. +/// +/// # Safety +/// +/// Caller must ensure AVX2 + SSE4.1 are available. +#[target_feature(enable = "avx2")] +pub(super) unsafe fn f64_to_f32_nearest( + simd: V3, + src: &[f64], + dst: &mut [f32], + error_on_overflow: bool, +) -> Result<(), crate::CastError> { + let _ = simd; + let n = src.len(); + let simd_len = n / 4 * 4; + + // Pass 1: branch-free narrowing + for i in (0..simd_len).step_by(4) { + let v = _mm256_loadu_pd(src.as_ptr().add(i)); + // _mm256_cvtpd_ps: 4 f64 → 4 f32 (returns __m128) + let narrowed = _mm256_cvtpd_ps(v); + _mm_storeu_ps(dst.as_mut_ptr().add(i), narrowed); + } + for i in simd_len..n { + dst[i] = src[i] as f32; + } + + // Pass 2: overflow check + if error_on_overflow { + let inf_ps = _mm_set1_ps(f32::INFINITY); + for i in (0..simd_len).step_by(4) { + let result = _mm_loadu_ps(dst.as_ptr().add(i)); + let abs_result = _mm_andnot_ps(_mm_set1_ps(-0.0), result); + let is_inf = _mm_cmpeq_ps(abs_result, inf_ps); + if _mm_movemask_ps(is_inf) != 0 { + for (&sv, &dv) in src[i..].iter().zip(dst[i..].iter()).take(4) { + if sv.is_finite() && dv.is_infinite() { + return Err(crate::CastError::OutOfRange { + value: sv, + lo: f32::MIN as f64, + hi: f32::MAX as f64, + }); + } + } + } + } + for i in simd_len..n { + if src[i].is_finite() && dst[i].is_infinite() { + return Err(crate::CastError::OutOfRange { + value: src[i], + lo: f32::MIN as f64, + hi: f32::MAX as f64, + }); + } + } + } + + Ok(()) +} + +/// Convert f64 slice to i32 slice with rounding, error on out-of-range. +/// +/// Same pipeline as `f64_to_i32_clamp` but checks range instead of clamping. +/// +/// # Safety +/// +/// Caller must ensure AVX2 + SSE4.1 are available. +#[target_feature(enable = "avx2")] +pub(super) unsafe fn f64_to_i32_check( + simd: V3, + src: &[f64], + dst: &mut [i32], + rounding: RoundingMode, +) -> Result<(), crate::CastError> { + let _ = simd; + let round_mode = avx2_round_mode(rounding); + let n = src.len(); + let simd_len = n / 4 * 4; + + let lo = _mm256_set1_pd(i32::MIN as f64); + let hi = _mm256_set1_pd(i32::MAX as f64); + + for i in (0..simd_len).step_by(4) { + let v = _mm256_loadu_pd(src.as_ptr().add(i)); + + // NaN check + let nan_mask = _mm256_cmp_pd(v, v, _CMP_UNORD_Q); + if _mm256_movemask_pd(nan_mask) != 0 { + for &val in &src[i..std::cmp::min(i + 4, n)] { + if val.is_nan() { + return Err(crate::CastError::NanOrInf { value: val }); + } + } + } + + let r = round_f64(v, round_mode); + + // Range check: error if any value < lo or > hi + let below = _mm256_cmp_pd(r, lo, _CMP_LT_OQ); + let above = _mm256_cmp_pd(r, hi, _CMP_GT_OQ); + let out_of_range = _mm256_or_pd(below, above); + if _mm256_movemask_pd(out_of_range) != 0 { + for &val in &src[i..std::cmp::min(i + 4, n)] { + let rounded = match rounding { + RoundingMode::NearestEven => val.round_ties_even(), + RoundingMode::TowardsZero => val.trunc(), + RoundingMode::TowardsPositive => val.ceil(), + RoundingMode::TowardsNegative => val.floor(), + RoundingMode::NearestAway => unreachable!(), + }; + if rounded < i32::MIN as f64 || rounded > i32::MAX as f64 { + return Err(crate::CastError::OutOfRange { + value: val, + lo: i32::MIN as f64, + hi: i32::MAX as f64, + }); + } + } + } + + let converted = _mm256_cvtpd_epi32(r); + _mm_storeu_si128(dst.as_mut_ptr().add(i) as *mut __m128i, converted); + } + + // Scalar tail + for i in simd_len..n { + let val = src[i]; + if val.is_nan() { + return Err(crate::CastError::NanOrInf { value: val }); + } + let rounded = match rounding { + RoundingMode::NearestEven => val.round_ties_even(), + RoundingMode::TowardsZero => val.trunc(), + RoundingMode::TowardsPositive => val.ceil(), + RoundingMode::TowardsNegative => val.floor(), + RoundingMode::NearestAway => unreachable!(), + }; + if rounded < i32::MIN as f64 || rounded > i32::MAX as f64 { + return Err(crate::CastError::OutOfRange { + value: val, + lo: i32::MIN as f64, + hi: i32::MAX as f64, + }); + } + dst[i] = rounded as i32; + } + + Ok(()) +} + // --------------------------------------------------------------------------- // Rounding helpers // --------------------------------------------------------------------------- diff --git a/core/src/simd/generic.rs b/core/src/simd/generic.rs index c543a8e..8a04768 100644 --- a/core/src/simd/generic.rs +++ b/core/src/simd/generic.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] //! Generic SIMD kernels using [`pulp`]'s [`WithSimd`] trait. //! //! These kernels use portable SIMD abstractions that compile to efficient code @@ -62,7 +63,7 @@ struct F64ToU8ClampKernel<'a> { rounding: RoundingMode, } -impl<'a> WithSimd for F64ToU8ClampKernel<'a> { +impl WithSimd for F64ToU8ClampKernel<'_> { type Output = Result<(), crate::CastError>; fn with_simd(self, simd: S) -> Self::Output { @@ -136,7 +137,7 @@ struct F64ToI32ClampKernel<'a> { rounding: RoundingMode, } -impl<'a> WithSimd for F64ToI32ClampKernel<'a> { +impl WithSimd for F64ToI32ClampKernel<'_> { type Output = Result<(), crate::CastError>; fn with_simd(self, simd: S) -> Self::Output { @@ -200,7 +201,7 @@ struct F32ToU8ClampKernel<'a> { rounding: RoundingMode, } -impl<'a> WithSimd for F32ToU8ClampKernel<'a> { +impl WithSimd for F32ToU8ClampKernel<'_> { type Output = Result<(), crate::CastError>; fn with_simd(self, simd: S) -> Self::Output { diff --git a/core/src/simd/mod.rs b/core/src/simd/mod.rs index ce2ead0..5e6f773 100644 --- a/core/src/simd/mod.rs +++ b/core/src/simd/mod.rs @@ -78,6 +78,37 @@ pub fn try_f32_to_u8_clamp( dispatch_f32_to_u8(src, dst, rounding) } +/// Try to convert f64 slice to f32 slice using SIMD with nearest-even rounding. +/// +/// When `error_on_overflow` is true, returns `Err(OutOfRange)` if any finite +/// f64 overflows to ±Inf in f32. When false, overflow to ±Inf is accepted. +/// +/// # Preconditions +/// +/// * `src.len() == dst.len()` +pub fn try_f64_to_f32_nearest( + src: &[f64], + dst: &mut [f32], + error_on_overflow: bool, +) -> Result { + dispatch_f64_to_f32(src, dst, error_on_overflow) +} + +/// Try to convert f64 slice to i32 slice using SIMD, returning an error +/// if any value is out of range (no clamping). +/// +/// # Preconditions +/// +/// * `src.len() == dst.len()` +/// * `rounding` is not `NearestAway` (caller must check) +pub fn try_f64_to_i32_check( + src: &[f64], + dst: &mut [i32], + rounding: RoundingMode, +) -> Result { + dispatch_f64_to_i32_check(src, dst, rounding) +} + // --------------------------------------------------------------------------- // Architecture dispatch — each function selects the optimal kernel. // --------------------------------------------------------------------------- @@ -247,6 +278,123 @@ fn dispatch_f32_to_u8( generic::f32_to_u8_clamp(src, dst, rounding) } +// --------------------------------------------------------------------------- +// Dispatch: f64 → f32 (nearest-even) +// --------------------------------------------------------------------------- + +#[cfg(target_arch = "x86_64")] +fn dispatch_f64_to_f32( + src: &[f64], + dst: &mut [f32], + error_on_overflow: bool, +) -> Result { + if let pulp::x86::Arch::V3(simd) = pulp::x86::Arch::new() { + return unsafe { avx2::f64_to_f32_nearest(simd, src, dst, error_on_overflow) } + .map(|()| true); + } + if is_x86_feature_detected!("avx") { + return unsafe { avx::f64_to_f32_nearest(src, dst, error_on_overflow) }.map(|()| true); + } + Ok(false) +} + +#[cfg(target_arch = "aarch64")] +fn dispatch_f64_to_f32( + src: &[f64], + dst: &mut [f32], + error_on_overflow: bool, +) -> Result { + unsafe { aarch64::f64_to_f32_nearest(src, dst, error_on_overflow) }.map(|()| true) +} + +#[cfg(target_arch = "wasm32")] +fn dispatch_f64_to_f32( + src: &[f64], + dst: &mut [f32], + error_on_overflow: bool, +) -> Result { + #[cfg(target_feature = "simd128")] + { + return unsafe { wasm32::f64_to_f32_nearest(src, dst, error_on_overflow) }.map(|()| true); + } + #[cfg(not(target_feature = "simd128"))] + { + let _ = (src, dst, error_on_overflow); + Ok(false) + } +} + +#[cfg(not(any( + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "wasm32" +)))] +fn dispatch_f64_to_f32( + _src: &[f64], + _dst: &mut [f32], + _error_on_overflow: bool, +) -> Result { + Ok(false) +} + +// --------------------------------------------------------------------------- +// Dispatch: f64 → i32 (range-check, no clamp) +// --------------------------------------------------------------------------- + +#[cfg(target_arch = "x86_64")] +fn dispatch_f64_to_i32_check( + src: &[f64], + dst: &mut [i32], + rounding: RoundingMode, +) -> Result { + if let pulp::x86::Arch::V3(simd) = pulp::x86::Arch::new() { + return unsafe { avx2::f64_to_i32_check(simd, src, dst, rounding) }.map(|()| true); + } + if is_x86_feature_detected!("avx") { + return unsafe { avx::f64_to_i32_check(src, dst, rounding) }.map(|()| true); + } + Ok(false) +} + +#[cfg(target_arch = "aarch64")] +fn dispatch_f64_to_i32_check( + src: &[f64], + dst: &mut [i32], + rounding: RoundingMode, +) -> Result { + unsafe { aarch64::f64_to_i32_check(src, dst, rounding) }.map(|()| true) +} + +#[cfg(target_arch = "wasm32")] +fn dispatch_f64_to_i32_check( + src: &[f64], + dst: &mut [i32], + rounding: RoundingMode, +) -> Result { + #[cfg(target_feature = "simd128")] + { + return unsafe { wasm32::f64_to_i32_check(src, dst, rounding) }.map(|()| true); + } + #[cfg(not(target_feature = "simd128"))] + { + let _ = (src, dst, rounding); + Ok(false) + } +} + +#[cfg(not(any( + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "wasm32" +)))] +fn dispatch_f64_to_i32_check( + _src: &[f64], + _dst: &mut [i32], + _rounding: RoundingMode, +) -> Result { + Ok(false) +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- diff --git a/core/src/simd/wasm32.rs b/core/src/simd/wasm32.rs index f81c666..9bada90 100644 --- a/core/src/simd/wasm32.rs +++ b/core/src/simd/wasm32.rs @@ -193,6 +193,138 @@ pub(super) unsafe fn f64_to_i32_clamp( Ok(()) } +// --------------------------------------------------------------------------- +// f64 → f32 narrowing +// --------------------------------------------------------------------------- + +/// Convert f64 slice to f32 slice using nearest-even (two-pass, WASM SIMD128). +/// +/// `f32x4_demote_f64x2_zero` narrows 2 f64 → 2 f32 (lower lanes of f32x4). +/// +/// # Safety +/// +/// Caller must ensure WASM SIMD128 feature is available. +pub(super) unsafe fn f64_to_f32_nearest( + src: &[f64], + dst: &mut [f32], + error_on_overflow: bool, +) -> Result<(), crate::CastError> { + let n = src.len(); + let simd_len = n / 2 * 2; + + // Pass 1: branch-free narrowing + for i in (0..simd_len).step_by(2) { + let v = v128_load(src.as_ptr().add(i) as *const v128); + // f32x4_demote_f64x2_zero: 2 f64 → lower 2 lanes of f32x4 + let narrowed = f32x4_demote_f64x2_zero(v); + // Extract and store 2 f32 values + dst[i] = f32x4_extract_lane::<0>(narrowed); + dst[i + 1] = f32x4_extract_lane::<1>(narrowed); + } + for i in simd_len..n { + dst[i] = src[i] as f32; + } + + // Pass 2: overflow check + if error_on_overflow { + for i in 0..n { + if src[i].is_finite() && dst[i].is_infinite() { + return Err(crate::CastError::OutOfRange { + value: src[i], + lo: f32::MIN as f64, + hi: f32::MAX as f64, + }); + } + } + } + + Ok(()) +} + +// --------------------------------------------------------------------------- +// f64 → i32 with range check +// --------------------------------------------------------------------------- + +/// Convert f64 slice to i32 slice with rounding, error on out-of-range (WASM SIMD128). +/// +/// # Safety +/// +/// Caller must ensure WASM SIMD128 feature is available. +pub(super) unsafe fn f64_to_i32_check( + src: &[f64], + dst: &mut [i32], + rounding: RoundingMode, +) -> Result<(), crate::CastError> { + let mut i = 0; + let len = src.len(); + + let lo_val = i32::MIN as f64; + let hi_val = i32::MAX as f64; + + while i + 2 <= len { + let v = v128_load(src.as_ptr().add(i) as *const v128); + + if any_nan_f64x2(v) { + for &val in &src[i..i + 2] { + if val.is_nan() { + return Err(crate::CastError::NanOrInf { value: val }); + } + } + } + + let rounded = round_f64x2(v, rounding); + + // Range check via lane extraction (WASM lacks f64x2 compare-to-mask) + let r0 = f64x2_extract_lane::<0>(rounded); + let r1 = f64x2_extract_lane::<1>(rounded); + if r0 < lo_val || r0 > hi_val { + return Err(crate::CastError::OutOfRange { + value: f64x2_extract_lane::<0>(v), + lo: lo_val, + hi: hi_val, + }); + } + if r1 < lo_val || r1 > hi_val { + return Err(crate::CastError::OutOfRange { + value: f64x2_extract_lane::<1>(v), + lo: lo_val, + hi: hi_val, + }); + } + + let i32_vals = i32x4_trunc_sat_f64x2_zero(rounded); + dst[i] = i32x4_extract_lane::<0>(i32_vals); + dst[i + 1] = i32x4_extract_lane::<1>(i32_vals); + + i += 2; + } + + // Scalar tail + for idx in i..len { + let val = src[idx]; + if val.is_nan() { + return Err(crate::CastError::NanOrInf { value: val }); + } + let rounded = match rounding { + RoundingMode::NearestEven => val.round_ties_even(), + RoundingMode::TowardsZero => val.trunc(), + RoundingMode::TowardsPositive => val.ceil(), + RoundingMode::TowardsNegative => val.floor(), + RoundingMode::NearestAway => unreachable!(), + }; + if rounded < lo_val || rounded > hi_val { + return Err(crate::CastError::OutOfRange { + value: val, + lo: lo_val, + hi: hi_val, + }); + } + dst[idx] = rounded as i32; + } + + Ok(()) +} + // --------------------------------------------------------------------------- // f32 → u8 with clamping // --------------------------------------------------------------------------- diff --git a/python/src/lib.rs b/python/src/lib.rs index 23a64dc..f604ede 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -297,8 +297,8 @@ fn do_float_to_float_alloc<'py, Src, Dst>( tgt_dtype: &str, ) -> PyResult where - Src: CastFloat + CastInto + ExtractFromPy + numpy::Element, - Dst: CastFloat + ExtractFromPy + numpy::Element, + Src: CastFloat + CastInto + ExtractFromPy + numpy::Element + 'static, + Dst: CastFloat + ExtractFromPy + numpy::Element + 'static, { let input_arr: PyReadonlyArrayDyn<'_, Src> = arr.extract()?; let src_slice = input_arr @@ -447,8 +447,8 @@ fn do_float_to_float_into<'py, Src, Dst>( tgt_dtype: &str, ) -> PyResult where - Src: CastFloat + CastInto + ExtractFromPy + numpy::Element, - Dst: CastFloat + ExtractFromPy + numpy::Element, + Src: CastFloat + CastInto + ExtractFromPy + numpy::Element + 'static, + Dst: CastFloat + ExtractFromPy + numpy::Element + 'static, { let input_arr: PyReadonlyArrayDyn<'_, Src> = arr.extract()?; let src_slice = input_arr