Skip to main content

oxigeo_core/buffer/
element.rs

1// The same-type fast path reinterprets a caller-owned `&mut [T]` as raw bytes so
2// the copy lowers to a single `memcpy`; that needs a small amount of `unsafe`.
3// Every `unsafe` block in this file is justified inline.
4#![allow(unsafe_code)]
5
6//! Typed raster elements and allocation-free bulk conversions.
7//!
8//! This module is the answer to "how do I read a band straight into my own
9//! `Vec<f64>` / `ndarray::Array2<f64>` without an extra full-size allocation?".
10//!
11//! * [`RasterElement`] associates a Rust primitive with its [`RasterDataType`]
12//!   and defines exact conversion semantics.
13//! * [`convert_raw_into`] converts raw driver bytes into a **caller-supplied**
14//!   `&mut [T]` in a single pass, with no intermediate buffer.
15//! * [`convert_raw_bytes`] is the byte-to-byte variant used by
16//!   [`RasterBuffer::convert_to`](crate::buffer::RasterBuffer::convert_to).
17//!
18//! # Conversion semantics
19//!
20//! | source → destination | behaviour |
21//! |---|---|
22//! | same type | `memcpy`, bit-for-bit |
23//! | integer → integer | **saturating**, exact (bridged through `i128`, never through `f64`) |
24//! | integer → float | `as` cast, round-to-nearest-even (IEEE 754 default) |
25//! | float → float | `as` cast, round-to-nearest-even |
26//! | float → integer | **saturating**, [`FloatToIntRounding::Nearest`] (half away from zero) by default |
27//!
28//! Special float inputs converting to an integer destination:
29//!
30//! | input | result |
31//! |---|---|
32//! | `NaN` | `0` |
33//! | `+∞` / value above `T::MAX` | `T::MAX` |
34//! | `-∞` / value below `T::MIN` | `T::MIN` |
35//!
36//! Nothing ever wraps, traps or produces undefined behaviour.
37//!
38//! # Precision
39//!
40//! Integer → integer conversions go through `i128`, so `u64`/`i64` values beyond
41//! 2<sup>53</sup> survive **exactly** — unlike the historical
42//! `get_pixel`/`set_pixel` path, which bridged everything through `f64`.
43//!
44//! Conversions whose *destination* is a float are still bounded by the
45//! destination's mantissa: a `u64`/`i64` magnitude above 2<sup>53</sup> cannot be
46//! represented exactly in `f64` (above 2<sup>24</sup> for `f32`) and is rounded
47//! to the nearest representable value. This is inherent to the destination type,
48//! not an artefact of the conversion.
49//!
50//! # Complex data types
51//!
52//! [`RasterDataType::CFloat32`] and [`RasterDataType::CFloat64`] have no
53//! `RasterElement` impl (they are not scalars). They are still accepted by
54//! [`convert_raw_into`] and [`convert_raw_bytes`] as *source* types, where the
55//! **real component** is used — matching
56//! [`RasterBuffer::get_pixel`](crate::buffer::RasterBuffer::get_pixel). As a
57//! *destination* (only reachable through [`convert_raw_bytes`]) the real
58//! component is written and the imaginary component is zeroed.
59//!
60//! # Examples
61//!
62//! ```
63//! use oxigeo_core::buffer::convert_raw_into;
64//! use oxigeo_core::types::RasterDataType;
65//!
66//! // Raw bytes exactly as a driver hands them over (native-endian UInt16).
67//! let raw: Vec<u8> = [1u16, 2, 3, 60_000]
68//!     .iter()
69//!     .flat_map(|v| v.to_ne_bytes())
70//!     .collect();
71//!
72//! // Destination owned by the caller — one allocation, and it is *yours*.
73//! let mut pixels = vec![0.0f64; 4];
74//! convert_raw_into(&raw, RasterDataType::UInt16, &mut pixels)?;
75//!
76//! assert_eq!(pixels, vec![1.0, 2.0, 3.0, 60_000.0]);
77//! # Ok::<(), oxigeo_core::error::OxiGeoError>(())
78//! ```
79
80#[cfg(not(feature = "std"))]
81use crate::compat::*;
82use crate::error::{OxiGeoError, Result};
83#[cfg(not(feature = "std"))]
84use crate::math::FloatExt;
85use crate::types::RasterDataType;
86
87mod private {
88    /// Prevents out-of-crate implementations of [`super::RasterElement`].
89    pub trait Sealed {}
90}
91
92/// Broad category of a [`RasterElement`], used to pick an exact conversion path.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub enum RasterElementKind {
95    /// A signed or unsigned integer (`u8` … `i64`).
96    Integer,
97    /// An IEEE 754 binary floating-point number (`f32`, `f64`).
98    Float,
99}
100
101/// Rounding applied when a floating-point sample is stored into an integer type.
102///
103/// Saturation at the destination bounds always happens, independent of this
104/// setting; only the treatment of the fractional part differs.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
106pub enum FloatToIntRounding {
107    /// Round to the nearest integer, halves away from zero
108    /// (`2.5 → 3`, `-2.5 → -3`). This matches GDAL's `RasterIO` behaviour and is
109    /// the default for every public entry point that does not take an explicit
110    /// rounding mode.
111    #[default]
112    Nearest,
113    /// Truncate towards zero (`2.9 → 2`, `-2.9 → -2`), i.e. plain C-style
114    /// `(int)x` semantics.
115    ///
116    /// Used by [`RasterBuffer::convert_to`](crate::buffer::RasterBuffer::convert_to)
117    /// so that its results stay bit-for-bit identical to the historical
118    /// per-pixel implementation.
119    Truncate,
120}
121
122/// A Rust primitive that can appear as a raster sample.
123///
124/// Implemented for exactly the ten scalar raster types — `u8`, `i8`, `u16`,
125/// `i16`, `u32`, `i32`, `u64`, `i64`, `f32` and `f64` — and sealed, so the set
126/// cannot grow outside this crate.
127///
128/// # Thread safety
129///
130/// Every implementor is a primitive scalar, so `Send + Sync` is part of the
131/// contract. Generic code bounded on `RasterElement` may therefore split a
132/// `&mut [T]` across worker threads without restating the bound — which is what
133/// lets the GeoTIFF driver run its *typed* band reads on rayon workers, not just
134/// the raw-byte ones. Because the trait is sealed the guarantee costs nothing:
135/// no out-of-crate type can ever weaken it.
136///
137/// Use it as a bound to write code that is generic over the pixel type:
138///
139/// ```
140/// use oxigeo_core::buffer::{RasterElement, convert_raw_into};
141/// use oxigeo_core::error::Result;
142/// use oxigeo_core::types::RasterDataType;
143///
144/// fn decode<T: RasterElement>(raw: &[u8], src: RasterDataType, dst: &mut [T]) -> Result<()> {
145///     convert_raw_into(raw, src, dst)
146/// }
147///
148/// let mut out = [0i32; 2];
149/// decode(&[1u8, 2], RasterDataType::UInt8, &mut out)?;
150/// assert_eq!(out, [1, 2]);
151/// assert_eq!(i32::DATA_TYPE, RasterDataType::Int32);
152/// # Ok::<(), oxigeo_core::error::OxiGeoError>(())
153/// ```
154pub trait RasterElement: Copy + Default + Send + Sync + 'static + private::Sealed {
155    /// The [`RasterDataType`] this Rust type stores.
156    const DATA_TYPE: RasterDataType;
157
158    /// Whether this type is an integer or a float.
159    const KIND: RasterElementKind;
160
161    /// Size of one sample in bytes (always `size_of::<Self>()`).
162    const SIZE: usize = core::mem::size_of::<Self>();
163
164    /// The fixed-size native-endian byte array for this type
165    /// (`[u8; size_of::<Self>()]`).
166    type Bytes: AsRef<[u8]> + AsMut<[u8]> + Copy + Default + for<'a> TryFrom<&'a [u8]>;
167
168    /// Decodes a sample from its native-endian byte array.
169    fn from_ne_bytes(bytes: Self::Bytes) -> Self;
170
171    /// Encodes a sample into its native-endian byte array.
172    fn to_ne_bytes(self) -> Self::Bytes;
173
174    /// Widens the sample to `f64`.
175    ///
176    /// Exact for every type except `u64`/`i64` magnitudes beyond
177    /// 2<sup>53</sup>, which are rounded to the nearest representable `f64`.
178    fn to_raster_f64(self) -> f64;
179
180    /// Widens the sample to `i128`.
181    ///
182    /// Exact for every integer type. For floats the value is rounded half away
183    /// from zero and saturated at the `i128` bounds; `NaN` becomes `0`.
184    fn to_raster_i128(self) -> i128;
185
186    /// Narrows an `f64` to this type, saturating at the type bounds and
187    /// rounding halves away from zero.
188    ///
189    /// `NaN` becomes `0` for integer destinations; float destinations keep
190    /// `NaN`/`±∞` as-is.
191    fn from_raster_f64(value: f64) -> Self;
192
193    /// Narrows an `f64` to this type, saturating at the type bounds and
194    /// truncating towards zero (C-style `(int)x`).
195    ///
196    /// Identical to [`RasterElement::from_raster_f64`] for float destinations.
197    fn from_raster_f64_truncating(value: f64) -> Self;
198
199    /// Narrows an `i128` to this type, saturating at the type bounds.
200    ///
201    /// Exact for integer destinations. Float destinations round to nearest.
202    fn from_raster_i128(value: i128) -> Self;
203
204    /// Decodes a sample from the first [`SIZE`](RasterElement::SIZE) bytes of
205    /// `bytes` (native endian).
206    ///
207    /// Longer slices are truncated — this is what makes complex sources work,
208    /// where each stride holds `[real, imaginary]` and only the real component
209    /// is read. Shorter slices yield [`Default::default`] instead of panicking.
210    #[inline]
211    fn from_ne_slice(bytes: &[u8]) -> Self {
212        match bytes
213            .get(..Self::SIZE)
214            .and_then(|head| <Self::Bytes as TryFrom<&[u8]>>::try_from(head).ok())
215        {
216            Some(raw) => Self::from_ne_bytes(raw),
217            None => Self::default(),
218        }
219    }
220}
221
222/// Rounds half away from zero without relying on `f64::round`, which is
223/// unavailable in `no_std` builds.
224///
225/// Unlike the `(x + 0.5).floor()` shortcut this is exact for every input:
226/// values at or above 2<sup>52</sup> are already integral and pass through
227/// unchanged, and `NaN`/`±∞` fall through to `floor`'s own result.
228#[inline]
229fn round_half_away_from_zero(value: f64) -> f64 {
230    let floor = value.floor();
231    // `value - floor(value)` is exact (both live in the same binade), so no
232    // rounding error can flip the comparison below.
233    let fract = value - floor;
234    if value.is_sign_negative() {
235        // Halves must move away from zero, i.e. stay on `floor`: -2.5 -> -3.
236        if fract > 0.5 { floor + 1.0 } else { floor }
237    } else if fract >= 0.5 {
238        floor + 1.0
239    } else {
240        floor
241    }
242}
243
244macro_rules! impl_raster_element_int {
245    ($($ty:ty => $variant:ident, $len:literal;)*) => {
246        $(
247            impl private::Sealed for $ty {}
248
249            impl RasterElement for $ty {
250                const DATA_TYPE: RasterDataType = RasterDataType::$variant;
251                const KIND: RasterElementKind = RasterElementKind::Integer;
252                type Bytes = [u8; $len];
253
254                #[inline]
255                fn from_ne_bytes(bytes: Self::Bytes) -> Self {
256                    <$ty>::from_ne_bytes(bytes)
257                }
258
259                #[inline]
260                fn to_ne_bytes(self) -> Self::Bytes {
261                    <$ty>::to_ne_bytes(self)
262                }
263
264                #[inline]
265                fn to_raster_f64(self) -> f64 {
266                    self as f64
267                }
268
269                #[inline]
270                fn to_raster_i128(self) -> i128 {
271                    self as i128
272                }
273
274                #[inline]
275                fn from_raster_f64(value: f64) -> Self {
276                    // `as` from float to int already saturates and maps NaN to 0.
277                    round_half_away_from_zero(value) as $ty
278                }
279
280                #[inline]
281                fn from_raster_f64_truncating(value: f64) -> Self {
282                    value as $ty
283                }
284
285                #[inline]
286                fn from_raster_i128(value: i128) -> Self {
287                    // Both bounds are exactly representable in i128 for every
288                    // integer type, so the clamp is lossless.
289                    value.clamp(<$ty>::MIN as i128, <$ty>::MAX as i128) as $ty
290                }
291            }
292        )*
293    };
294}
295
296macro_rules! impl_raster_element_float {
297    ($($ty:ty => $variant:ident, $len:literal;)*) => {
298        $(
299            impl private::Sealed for $ty {}
300
301            impl RasterElement for $ty {
302                const DATA_TYPE: RasterDataType = RasterDataType::$variant;
303                const KIND: RasterElementKind = RasterElementKind::Float;
304                type Bytes = [u8; $len];
305
306                #[inline]
307                fn from_ne_bytes(bytes: Self::Bytes) -> Self {
308                    <$ty>::from_ne_bytes(bytes)
309                }
310
311                #[inline]
312                fn to_ne_bytes(self) -> Self::Bytes {
313                    <$ty>::to_ne_bytes(self)
314                }
315
316                #[inline]
317                fn to_raster_f64(self) -> f64 {
318                    self as f64
319                }
320
321                #[inline]
322                fn to_raster_i128(self) -> i128 {
323                    round_half_away_from_zero(self as f64) as i128
324                }
325
326                #[inline]
327                fn from_raster_f64(value: f64) -> Self {
328                    value as $ty
329                }
330
331                #[inline]
332                fn from_raster_f64_truncating(value: f64) -> Self {
333                    value as $ty
334                }
335
336                #[inline]
337                fn from_raster_i128(value: i128) -> Self {
338                    value as $ty
339                }
340            }
341        )*
342    };
343}
344
345impl_raster_element_int! {
346    u8 => UInt8, 1;
347    i8 => Int8, 1;
348    u16 => UInt16, 2;
349    i16 => Int16, 2;
350    u32 => UInt32, 4;
351    i32 => Int32, 4;
352    u64 => UInt64, 8;
353    i64 => Int64, 8;
354}
355
356impl_raster_element_float! {
357    f32 => Float32, 4;
358    f64 => Float64, 8;
359}
360
361/// Expands `$body!(rust_type, stride)` once per [`RasterDataType`].
362///
363/// The stride is the *pixel* size, which differs from the Rust type size for the
364/// complex types: their real component is the leading `f32`/`f64`.
365macro_rules! dispatch_data_type {
366    ($data_type:expr, $body:ident) => {
367        match $data_type {
368            RasterDataType::UInt8 => $body!(u8, 1),
369            RasterDataType::Int8 => $body!(i8, 1),
370            RasterDataType::UInt16 => $body!(u16, 2),
371            RasterDataType::Int16 => $body!(i16, 2),
372            RasterDataType::UInt32 => $body!(u32, 4),
373            RasterDataType::Int32 => $body!(i32, 4),
374            RasterDataType::UInt64 => $body!(u64, 8),
375            RasterDataType::Int64 => $body!(i64, 8),
376            RasterDataType::Float32 => $body!(f32, 4),
377            RasterDataType::Float64 => $body!(f64, 8),
378            RasterDataType::CFloat32 => $body!(f32, 8),
379            RasterDataType::CFloat64 => $body!(f64, 16),
380        }
381    };
382}
383
384/// Reinterprets raw driver bytes as a real `&[S]`, or `None` when that would be
385/// misaligned.
386///
387/// With a genuine `&[S]` in hand the conversion loop is `dst[i] = f(src[i])`
388/// over two primitive slices — the shape every autovectoriser recognises. An
389/// `f32 → f64` band then lowers to `fcvtl` on aarch64 and `cvtps2pd` on x86-64.
390///
391/// Assembling each sample from a byte chunk instead *can* vectorise just as well
392/// — with rustc 1.95 / LLVM 22 it demonstrably does, on both of those targets —
393/// but that depends on the optimiser seeing through `chunks_exact` plus a
394/// `TryFrom<&[u8]>` plus an `Option`. Going through the typed slice does not
395/// depend on anything: it is the same loop the "decode, then run a separate
396/// vectorised map" workaround runs, so no backend can make it slower than that.
397///
398/// Allocator-backed buffers are aligned in practice, but nothing *guarantees*
399/// it — a `Vec<u8>` only promises alignment 1, and sub-slices such as
400/// `&bytes[1..]` are routinely misaligned — so the caller must handle `None`.
401#[inline]
402fn samples_from_bytes<S: RasterElement>(src: &[u8]) -> Option<&[S]> {
403    if S::SIZE == 0 || !src.len().is_multiple_of(S::SIZE) {
404        return None;
405    }
406    // `align_offset` is permitted to answer `usize::MAX` ("cannot tell"), which
407    // this comparison reads as "not aligned". A conservative answer only costs
408    // the (equally fast) unaligned path, which is always correct.
409    if src.as_ptr().align_offset(core::mem::align_of::<S>()) != 0 {
410        return None;
411    }
412    // SAFETY: `S` is one of the ten primitive numeric types (the trait is
413    // sealed), so it has no padding and every bit pattern is a valid value —
414    // any run of `S::SIZE` initialised bytes is a valid `S`. The pointer is
415    // checked to be aligned for `S` just above, the length is an exact multiple
416    // of `S::SIZE`, so `src.len() / S::SIZE` elements cover exactly the same
417    // bytes as `src` (no overflow: the byte range already exists). The returned
418    // slice borrows `src` for the same lifetime and is shared, like `src`.
419    Some(unsafe { core::slice::from_raw_parts(src.as_ptr().cast::<S>(), src.len() / S::SIZE) })
420}
421
422/// [`samples_from_bytes`] for a destination buffer.
423#[inline]
424fn samples_from_bytes_mut<D: RasterElement>(dst: &mut [u8]) -> Option<&mut [D]> {
425    if D::SIZE == 0 || !dst.len().is_multiple_of(D::SIZE) {
426        return None;
427    }
428    if dst.as_ptr().align_offset(core::mem::align_of::<D>()) != 0 {
429        return None;
430    }
431    let count = dst.len() / D::SIZE;
432    // SAFETY: as `samples_from_bytes`, plus: the borrow is unique (`&mut [u8]`)
433    // and is consumed by this call, so the returned `&mut [D]` is the only way
434    // to reach those bytes for its lifetime. Every bit pattern of `D` is valid,
435    // so writes through it leave the buffer in a state that is still a valid
436    // `[u8]` afterwards.
437    Some(unsafe { core::slice::from_raw_parts_mut(dst.as_mut_ptr().cast::<D>(), count) })
438}
439
440/// How many whole samples of `SIZE` bytes a byte slice holds.
441#[inline]
442const fn sample_capacity(bytes: usize, size: usize) -> usize {
443    match bytes.checked_div(size) {
444        Some(count) => count,
445        None => 0,
446    }
447}
448
449/// The vectorisable kernel: convert `src[i]` into `dst[i]` for as many elements
450/// as both slices hold.
451///
452/// `S::KIND`/`D::KIND` are associated consts, so the dispatch below collapses at
453/// monomorphisation and the loop body is a single straight-line expression over
454/// two primitive slices.
455#[inline]
456fn convert_samples<S: RasterElement, D: RasterElement>(
457    src: &[S],
458    dst: &mut [D],
459    rounding: FloatToIntRounding,
460) {
461    macro_rules! run {
462        ($convert:expr) => {{
463            for (value, out) in src.iter().zip(dst.iter_mut()) {
464                *out = $convert(*value);
465            }
466        }};
467    }
468
469    match (S::KIND, D::KIND, rounding) {
470        (RasterElementKind::Integer, RasterElementKind::Integer, _) => {
471            run!(|value: S| D::from_raster_i128(value.to_raster_i128()));
472        }
473        (_, _, FloatToIntRounding::Nearest) => {
474            run!(|value: S| D::from_raster_f64(value.to_raster_f64()));
475        }
476        (_, _, FloatToIntRounding::Truncate) => {
477            run!(|value: S| D::from_raster_f64_truncating(value.to_raster_f64()));
478        }
479    }
480}
481
482/// [`convert_samples`] for a source that could not be reinterpreted in place.
483///
484/// Each sample is loaded with an *unaligned typed* read rather than assembled
485/// from a byte chunk. That keeps the loop a plain strided load + convert +
486/// store, which vectorises exactly like the aligned path (unaligned vector loads
487/// are full speed on every target this crate builds for), while staging through
488/// an intermediate buffer would add a whole extra pass over the data — measured
489/// at 1.7× slower on a 16 Mpx `f32 → f64` band.
490#[inline]
491fn convert_unaligned_samples<S: RasterElement, D: RasterElement>(
492    src: &[u8],
493    dst: &mut [D],
494    rounding: FloatToIntRounding,
495) {
496    let count = dst.len().min(sample_capacity(src.len(), S::SIZE));
497    let base = src.as_ptr();
498    let Some(dst) = dst.get_mut(..count) else {
499        return;
500    };
501
502    macro_rules! run {
503        ($convert:expr) => {{
504            for (index, out) in dst.iter_mut().enumerate() {
505                // SAFETY: `index < count` and `count * S::SIZE <= src.len()`, so
506                // the read stays inside `src`'s allocation. `read_unaligned`
507                // imposes no alignment requirement, and every bit pattern of `S`
508                // is a valid value (sealed trait: ten primitive numerics), so any
509                // `S::SIZE` initialised bytes decode to a valid `S` — the same
510                // value `S::from_ne_bytes` would produce, by the definition of
511                // the native-endian representation.
512                let value: S = unsafe { base.add(index * S::SIZE).cast::<S>().read_unaligned() };
513                *out = $convert(value);
514            }
515        }};
516    }
517
518    match (S::KIND, D::KIND, rounding) {
519        (RasterElementKind::Integer, RasterElementKind::Integer, _) => {
520            run!(|value: S| D::from_raster_i128(value.to_raster_i128()));
521        }
522        (_, _, FloatToIntRounding::Nearest) => {
523            run!(|value: S| D::from_raster_f64(value.to_raster_f64()));
524        }
525        (_, _, FloatToIntRounding::Truncate) => {
526            run!(|value: S| D::from_raster_f64_truncating(value.to_raster_f64()));
527        }
528    }
529}
530
531/// Converts a *contiguous* run of `S` samples (one sample every `S::SIZE` bytes)
532/// into `dst`.
533///
534/// Reinterprets the source in place when it is aligned and falls back to
535/// unaligned typed loads when it is not. Both paths run the same conversion
536/// expression on the same decoded value, so they are bit-identical by
537/// construction — and the test suite proves it on NaN payloads, signed zeroes,
538/// subnormals and every integer bound.
539#[inline]
540fn convert_contiguous<S: RasterElement, D: RasterElement>(
541    src: &[u8],
542    dst: &mut [D],
543    rounding: FloatToIntRounding,
544) {
545    match samples_from_bytes::<S>(src) {
546        Some(values) => convert_samples(values, dst, rounding),
547        None => convert_unaligned_samples::<S, D>(src, dst, rounding),
548    }
549}
550
551/// Byte-to-byte counterpart of [`convert_contiguous`] for a destination that
552/// could not be reinterpreted as `&mut [D]`.
553#[inline]
554fn convert_unaligned_bytes<S: RasterElement, D: RasterElement>(
555    src: &[u8],
556    dst: &mut [u8],
557    rounding: FloatToIntRounding,
558) {
559    let count = sample_capacity(src.len(), S::SIZE).min(sample_capacity(dst.len(), D::SIZE));
560    let base = src.as_ptr();
561    let out = dst.as_mut_ptr();
562
563    macro_rules! run {
564        ($convert:expr) => {{
565            for index in 0..count {
566                // SAFETY: `count` is clamped so that both `index * S::SIZE +
567                // S::SIZE <= src.len()` and `index * D::SIZE + D::SIZE <=
568                // dst.len()`, so both accesses stay inside their allocation.
569                // Neither read nor write requires alignment. Every bit pattern of
570                // `S` is a valid `S` and every bit pattern of `D` is a valid `u8`
571                // sequence, so both directions are well defined. `src` and `dst`
572                // are a shared and a unique borrow of distinct objects, so the
573                // write cannot invalidate the read.
574                let value: S = unsafe { base.add(index * S::SIZE).cast::<S>().read_unaligned() };
575                let converted: D = $convert(value);
576                unsafe {
577                    out.add(index * D::SIZE)
578                        .cast::<D>()
579                        .write_unaligned(converted);
580                }
581            }
582        }};
583    }
584
585    match (S::KIND, D::KIND, rounding) {
586        (RasterElementKind::Integer, RasterElementKind::Integer, _) => {
587            run!(|value: S| D::from_raster_i128(value.to_raster_i128()));
588        }
589        (_, _, FloatToIntRounding::Nearest) => {
590            run!(|value: S| D::from_raster_f64(value.to_raster_f64()));
591        }
592        (_, _, FloatToIntRounding::Truncate) => {
593            run!(|value: S| D::from_raster_f64_truncating(value.to_raster_f64()));
594        }
595    }
596}
597
598/// Core loop: decode `S` samples out of `src` (one every `src_stride` bytes) and
599/// write converted `D` values into `dst`.
600///
601/// `src` must hold at least `dst.len()` strides; callers validate that.
602#[inline]
603fn convert_into_typed<S: RasterElement, D: RasterElement, const SRC_STRIDE: usize>(
604    src: &[u8],
605    dst: &mut [D],
606    rounding: FloatToIntRounding,
607) {
608    // Scalar source: samples are back to back, so the whole run can go through
609    // the bulk path. `SRC_STRIDE` is a const generic, so this is resolved at
610    // monomorphisation and the other branch is not even compiled in.
611    if SRC_STRIDE == S::SIZE {
612        convert_contiguous::<S, D>(src, dst, rounding);
613        return;
614    }
615
616    // Strided source — only the complex data types reach here, where each stride
617    // holds `[real, imaginary]` and just the real component is read.
618    macro_rules! run {
619        ($convert:expr) => {{
620            for (chunk, out) in src.chunks_exact(SRC_STRIDE).zip(dst.iter_mut()) {
621                *out = $convert(S::from_ne_slice(chunk));
622            }
623        }};
624    }
625
626    match (S::KIND, D::KIND, rounding) {
627        (RasterElementKind::Integer, RasterElementKind::Integer, _) => {
628            run!(|value: S| D::from_raster_i128(value.to_raster_i128()));
629        }
630        (_, _, FloatToIntRounding::Nearest) => {
631            run!(|value: S| D::from_raster_f64(value.to_raster_f64()));
632        }
633        (_, _, FloatToIntRounding::Truncate) => {
634            run!(|value: S| D::from_raster_f64_truncating(value.to_raster_f64()));
635        }
636    }
637}
638
639/// Same as [`convert_into_typed`] but writing native-endian bytes, so that
640/// complex destination types (real component + zeroed imaginary component) are
641/// expressible.
642///
643/// Both strides are const generics: with `DST_STRIDE` and `D::SIZE` known at
644/// compile time the store becomes a fixed-size write and the imaginary-component
645/// fill disappears entirely for scalar destinations. Passing them as ordinary
646/// arguments made every element pay for an out-of-line `memcpy`/`memset` call
647/// (measured on 4.2M UInt16 → Float32 samples: 8.5 ms vs 0.59 ms).
648#[inline]
649fn convert_into_bytes<
650    S: RasterElement,
651    D: RasterElement,
652    const SRC_STRIDE: usize,
653    const DST_STRIDE: usize,
654>(
655    src: &[u8],
656    dst: &mut [u8],
657    rounding: FloatToIntRounding,
658) {
659    // Both sides contiguous scalars (i.e. neither is complex): route through the
660    // same bulk kernel the typed entry point uses. Both strides are const
661    // generics, so this test disappears at monomorphisation.
662    if SRC_STRIDE == S::SIZE && DST_STRIDE == D::SIZE {
663        match samples_from_bytes_mut::<D>(dst) {
664            Some(out) => convert_contiguous::<S, D>(src, out, rounding),
665            None => convert_unaligned_bytes::<S, D>(src, dst, rounding),
666        }
667        return;
668    }
669
670    macro_rules! run {
671        ($convert:expr) => {{
672            for (chunk, out) in src
673                .chunks_exact(SRC_STRIDE)
674                .zip(dst.chunks_exact_mut(DST_STRIDE))
675            {
676                let value: D = $convert(S::from_ne_slice(chunk));
677                let encoded = value.to_ne_bytes();
678                if let Some(slot) = out.get_mut(..D::SIZE) {
679                    if let Some(bytes) = encoded.as_ref().get(..D::SIZE) {
680                        slot.copy_from_slice(bytes);
681                    }
682                }
683                // Zero the imaginary component of complex destinations. Both
684                // sides are compile-time constants, so this vanishes for every
685                // scalar destination.
686                if DST_STRIDE > D::SIZE {
687                    if let Some(padding) = out.get_mut(D::SIZE..) {
688                        padding.fill(0);
689                    }
690                }
691            }
692        }};
693    }
694
695    match (S::KIND, D::KIND, rounding) {
696        (RasterElementKind::Integer, RasterElementKind::Integer, _) => {
697            run!(|value: S| D::from_raster_i128(value.to_raster_i128()));
698        }
699        (_, _, FloatToIntRounding::Nearest) => {
700            run!(|value: S| D::from_raster_f64(value.to_raster_f64()));
701        }
702        (_, _, FloatToIntRounding::Truncate) => {
703            run!(|value: S| D::from_raster_f64_truncating(value.to_raster_f64()));
704        }
705    }
706}
707
708/// "A raster data type claims to occupy zero bytes", built out of line.
709#[cold]
710#[inline(never)]
711fn zero_stride_error() -> OxiGeoError {
712    OxiGeoError::Internal {
713        message: "Raster data type reported a zero-byte sample size".to_string(),
714    }
715}
716
717/// "`src` is not a whole number of samples", built out of line.
718///
719/// `#[cold]`/`#[inline(never)]`: the `format!` machinery would otherwise be
720/// inlined into the validation, which is on the hottest path in the crate — the
721/// GeoTIFF driver runs it once per tile row.
722#[cold]
723#[inline(never)]
724fn partial_sample_error(src_len: usize, stride: usize) -> OxiGeoError {
725    OxiGeoError::InvalidParameter {
726        parameter: "src",
727        message: format!(
728            "Source length {} is not a whole number of {}-byte samples",
729            src_len, stride
730        ),
731    }
732}
733
734/// "`dst` has the wrong length", built out of line.
735#[cold]
736#[inline(never)]
737fn length_mismatch_error(
738    count: usize,
739    src_len: usize,
740    stride: usize,
741    dst_len: usize,
742) -> OxiGeoError {
743    OxiGeoError::InvalidParameter {
744        parameter: "dst",
745        message: format!(
746            "Destination length mismatch: source holds {} samples ({} bytes / {} bytes each), destination has {}",
747            count, src_len, stride, dst_len
748        ),
749    }
750}
751
752/// Validates that `src` holds exactly `dst_len` whole samples of `stride` bytes.
753fn sample_count(src_len: usize, stride: usize, dst_len: usize) -> Result<usize> {
754    if stride == 0 {
755        return Err(zero_stride_error());
756    }
757    if !src_len.is_multiple_of(stride) {
758        return Err(partial_sample_error(src_len, stride));
759    }
760    let count = src_len / stride;
761    if count != dst_len {
762        return Err(length_mismatch_error(count, src_len, stride, dst_len));
763    }
764    Ok(count)
765}
766
767/// [`sample_count`] with the stride known at compile time.
768///
769/// Same checks, same errors — but both divisions collapse to shifts. With a
770/// runtime stride they are two `udiv`s, and that is not free where it matters:
771/// the GeoTIFF driver converts one tile row per call, i.e. 62 500 calls for a
772/// 4000 × 4000 raster of 256 × 256 tiles. Measured on that call pattern, a
773/// runtime stride costs **25 ns per call — 1.6 ms per band read** — while a
774/// constant one costs nothing at all.
775#[inline]
776fn check_sample_count<const STRIDE: usize>(src_len: usize, dst_len: usize) -> Result<()> {
777    // Every instantiation passes a non-zero literal, so this folds away; it is
778    // here so that a zero can never reach the `%` below and panic.
779    if STRIDE == 0 {
780        return Err(zero_stride_error());
781    }
782    if !src_len.is_multiple_of(STRIDE) {
783        return Err(partial_sample_error(src_len, STRIDE));
784    }
785    let count = src_len / STRIDE;
786    if count != dst_len {
787        return Err(length_mismatch_error(count, src_len, STRIDE, dst_len));
788    }
789    Ok(())
790}
791
792/// The `src_type == T::DATA_TYPE` fast path: identical layout, so the whole
793/// conversion is one `memcpy`, bit for bit.
794#[inline]
795fn copy_same_type<T: RasterElement>(src: &[u8], dst: &mut [T]) {
796    // SAFETY: `T` is one of the ten primitive numeric types (sealed trait): it
797    // has no padding and every bit pattern is a valid value, so raw bytes may be
798    // written into it. The pointer comes from a live `&mut [T]`, and `u8` needs
799    // no alignment. The slice covers exactly `size_of_val(dst)` bytes of that
800    // same allocation.
801    let dst_bytes = unsafe {
802        core::slice::from_raw_parts_mut(dst.as_mut_ptr().cast::<u8>(), core::mem::size_of_val(dst))
803    };
804    // Callers validate the lengths; the guard makes the copy unable to panic
805    // even if that ever stopped being true.
806    if dst_bytes.len() == src.len() {
807        dst_bytes.copy_from_slice(src);
808    }
809}
810
811/// Reinterprets typed samples as their native-endian bytes.
812///
813/// Zero-copy: the returned slice borrows `values`. Useful when handing a typed
814/// array to a writer that speaks raw bytes.
815///
816/// # Examples
817///
818/// ```
819/// use oxigeo_core::buffer::elements_as_bytes;
820///
821/// let values = [1u16, 2u16];
822/// assert_eq!(elements_as_bytes(&values).len(), 4);
823/// ```
824#[must_use]
825pub fn elements_as_bytes<T: RasterElement>(values: &[T]) -> &[u8] {
826    // SAFETY: `T` is one of the ten primitive numeric types (the trait is
827    // sealed). Those have no padding bytes and no niches, so every byte of the
828    // slice is initialised and readable as `u8`. The resulting slice borrows
829    // `values` for the same lifetime and covers exactly the same bytes, and `u8`
830    // has an alignment of 1 so no alignment requirement can be violated.
831    unsafe {
832        core::slice::from_raw_parts(values.as_ptr().cast::<u8>(), core::mem::size_of_val(values))
833    }
834}
835
836/// Converts raw raster bytes of `src_type` into `dst` in a single pass, without
837/// allocating.
838///
839/// This is the primitive behind
840/// [`RasterBuffer::copy_to_slice`](crate::buffer::RasterBuffer::copy_to_slice):
841/// feed it the bytes a driver decoded and a destination the caller already owns
842/// (a `Vec<f64>`, an `ndarray::Array2<f64>`'s backing slice, a memory-mapped
843/// region…) and no intermediate buffer is ever materialised.
844///
845/// `src` does **not** need to be aligned for `T`: samples are decoded with
846/// native-endian byte reads.
847///
848/// Float samples are rounded with [`FloatToIntRounding::Nearest`]; use
849/// [`convert_raw_into_with`] to choose. See the [module documentation](self) for
850/// the full semantics.
851///
852/// # Errors
853///
854/// Returns [`OxiGeoError::InvalidParameter`] if `src` is not a whole number of
855/// `src_type` samples, or if `dst.len()` differs from that sample count.
856///
857/// # Examples
858///
859/// ```
860/// use oxigeo_core::buffer::convert_raw_into;
861/// use oxigeo_core::types::RasterDataType;
862///
863/// let raw: Vec<u8> = [-1i16, 0, 1].iter().flat_map(|v| v.to_ne_bytes()).collect();
864///
865/// let mut pixels = vec![0.0f64; 3];
866/// convert_raw_into(&raw, RasterDataType::Int16, &mut pixels)?;
867/// assert_eq!(pixels, vec![-1.0, 0.0, 1.0]);
868///
869/// // Wrong-sized destinations are rejected instead of truncating silently.
870/// let mut too_small = vec![0.0f64; 2];
871/// assert!(convert_raw_into(&raw, RasterDataType::Int16, &mut too_small).is_err());
872/// # Ok::<(), oxigeo_core::error::OxiGeoError>(())
873/// ```
874pub fn convert_raw_into<T: RasterElement>(
875    src: &[u8],
876    src_type: RasterDataType,
877    dst: &mut [T],
878) -> Result<()> {
879    convert_raw_into_with(src, src_type, dst, FloatToIntRounding::Nearest)
880}
881
882/// [`convert_raw_into`] with an explicit float→integer rounding mode.
883///
884/// # Errors
885///
886/// Same as [`convert_raw_into`].
887///
888/// # Examples
889///
890/// ```
891/// use oxigeo_core::buffer::{FloatToIntRounding, convert_raw_into_with};
892/// use oxigeo_core::types::RasterDataType;
893///
894/// let raw: Vec<u8> = [2.5f32, -2.5].iter().flat_map(|v| v.to_ne_bytes()).collect();
895///
896/// let mut nearest = [0i16; 2];
897/// convert_raw_into_with(&raw, RasterDataType::Float32, &mut nearest, FloatToIntRounding::Nearest)?;
898/// assert_eq!(nearest, [3, -3]);
899///
900/// let mut truncated = [0i16; 2];
901/// convert_raw_into_with(&raw, RasterDataType::Float32, &mut truncated, FloatToIntRounding::Truncate)?;
902/// assert_eq!(truncated, [2, -2]);
903/// # Ok::<(), oxigeo_core::error::OxiGeoError>(())
904/// ```
905pub fn convert_raw_into_with<T: RasterElement>(
906    src: &[u8],
907    src_type: RasterDataType,
908    dst: &mut [T],
909    rounding: FloatToIntRounding,
910) -> Result<()> {
911    // Dispatch *first*, then validate with the stride of the arm we landed in:
912    // `$stride` is a literal, so the length checks cost two shifts instead of
913    // two integer divisions. See [`check_sample_count`] for why that is worth
914    // doing.
915    macro_rules! body {
916        ($source:ty, $stride:literal) => {{
917            check_sample_count::<$stride>(src.len(), dst.len())?;
918            if src_type == T::DATA_TYPE {
919                // Same on-disk and in-memory layout: one memcpy. The comparison
920                // is between two constants inside each arm, so only the arm that
921                // can match keeps this branch at all.
922                copy_same_type(src, dst);
923            } else {
924                convert_into_typed::<$source, T, $stride>(src, dst, rounding);
925            }
926        }};
927    }
928    dispatch_data_type!(src_type, body);
929
930    Ok(())
931}
932
933/// Converts raw raster bytes of `src_type` into raw bytes of `dst_type`, in a
934/// single pass and without allocating.
935///
936/// The byte-level counterpart of [`convert_raw_into`]. Prefer the typed version
937/// whenever the destination type is known at compile time; this one exists for
938/// dynamically typed destinations and for the complex data types, which have no
939/// [`RasterElement`] impl.
940///
941/// Neither slice needs any particular alignment.
942///
943/// # Errors
944///
945/// Returns [`OxiGeoError::InvalidParameter`] if either slice is not a whole
946/// number of samples, or if the two sample counts differ.
947///
948/// # Examples
949///
950/// ```
951/// use oxigeo_core::buffer::{FloatToIntRounding, convert_raw_bytes};
952/// use oxigeo_core::types::RasterDataType;
953///
954/// let src: Vec<u8> = [200u16, 1000, 65_535]
955///     .iter()
956///     .flat_map(|v| v.to_ne_bytes())
957///     .collect();
958/// let mut dst = vec![0u8; 3]; // three UInt8 samples
959///
960/// convert_raw_bytes(
961///     &src,
962///     RasterDataType::UInt16,
963///     &mut dst,
964///     RasterDataType::UInt8,
965///     FloatToIntRounding::Nearest,
966/// )?;
967///
968/// // 1000 saturates to 255 instead of wrapping to 232.
969/// assert_eq!(dst, vec![200, 255, 255]);
970/// # Ok::<(), oxigeo_core::error::OxiGeoError>(())
971/// ```
972pub fn convert_raw_bytes(
973    src: &[u8],
974    src_type: RasterDataType,
975    dst: &mut [u8],
976    dst_type: RasterDataType,
977    rounding: FloatToIntRounding,
978) -> Result<()> {
979    let src_stride = src_type.size_bytes();
980    let dst_stride = dst_type.size_bytes();
981
982    if dst_stride == 0 {
983        return Err(OxiGeoError::Internal {
984            message: "Raster data type reported a zero-byte sample size".to_string(),
985        });
986    }
987    if !dst.len().is_multiple_of(dst_stride) {
988        return Err(OxiGeoError::InvalidParameter {
989            parameter: "dst",
990            message: format!(
991                "Destination length {} is not a whole number of {}-byte samples",
992                dst.len(),
993                dst_stride
994            ),
995        });
996    }
997    sample_count(src.len(), src_stride, dst.len() / dst_stride)?;
998
999    if src_type == dst_type {
1000        // Identical layout: straight memcpy, bit-for-bit (including the
1001        // imaginary components of complex types).
1002        dst.copy_from_slice(src);
1003        return Ok(());
1004    }
1005
1006    macro_rules! destination {
1007        ($dest:ty, $dst_stride:literal) => {{
1008            macro_rules! source {
1009                ($source:ty, $src_stride:literal) => {
1010                    convert_into_bytes::<$source, $dest, $src_stride, $dst_stride>(
1011                        src, dst, rounding,
1012                    )
1013                };
1014            }
1015            dispatch_data_type!(src_type, source)
1016        }};
1017    }
1018    dispatch_data_type!(dst_type, destination);
1019
1020    Ok(())
1021}
1022
1023#[cfg(test)]
1024mod tests {
1025    #![allow(clippy::expect_used)]
1026
1027    use super::*;
1028
1029    #[test]
1030    fn test_data_type_mapping() {
1031        assert_eq!(u8::DATA_TYPE, RasterDataType::UInt8);
1032        assert_eq!(i8::DATA_TYPE, RasterDataType::Int8);
1033        assert_eq!(u16::DATA_TYPE, RasterDataType::UInt16);
1034        assert_eq!(i16::DATA_TYPE, RasterDataType::Int16);
1035        assert_eq!(u32::DATA_TYPE, RasterDataType::UInt32);
1036        assert_eq!(i32::DATA_TYPE, RasterDataType::Int32);
1037        assert_eq!(u64::DATA_TYPE, RasterDataType::UInt64);
1038        assert_eq!(i64::DATA_TYPE, RasterDataType::Int64);
1039        assert_eq!(f32::DATA_TYPE, RasterDataType::Float32);
1040        assert_eq!(f64::DATA_TYPE, RasterDataType::Float64);
1041    }
1042
1043    #[test]
1044    fn test_elements_are_send_and_sync() {
1045        // Proves the bound is reachable *through* the trait: generic code that
1046        // only knows `T: RasterElement` may hand `T` to another thread. This is
1047        // what allows the GeoTIFF driver's typed band reads to split a
1048        // `&mut [T]` across rayon workers.
1049        const fn assert_send_sync<T: Send + Sync>() {}
1050        const fn assert_element<T: RasterElement>() {
1051            assert_send_sync::<T>();
1052        }
1053        assert_element::<u8>();
1054        assert_element::<i8>();
1055        assert_element::<u16>();
1056        assert_element::<i16>();
1057        assert_element::<u32>();
1058        assert_element::<i32>();
1059        assert_element::<u64>();
1060        assert_element::<i64>();
1061        assert_element::<f32>();
1062        assert_element::<f64>();
1063    }
1064
1065    #[test]
1066    fn test_size_matches_data_type() {
1067        assert_eq!(u8::SIZE, RasterDataType::UInt8.size_bytes());
1068        assert_eq!(i16::SIZE, RasterDataType::Int16.size_bytes());
1069        assert_eq!(f32::SIZE, RasterDataType::Float32.size_bytes());
1070        assert_eq!(f64::SIZE, RasterDataType::Float64.size_bytes());
1071        assert_eq!(u64::SIZE, RasterDataType::UInt64.size_bytes());
1072    }
1073
1074    #[test]
1075    fn test_round_half_away_from_zero() {
1076        assert_eq!(round_half_away_from_zero(2.5), 3.0);
1077        assert_eq!(round_half_away_from_zero(-2.5), -3.0);
1078        assert_eq!(round_half_away_from_zero(2.4999), 2.0);
1079        assert_eq!(round_half_away_from_zero(-2.4999), -2.0);
1080        assert_eq!(round_half_away_from_zero(0.0), 0.0);
1081        assert_eq!(round_half_away_from_zero(-0.4), -0.0);
1082        // Largest f64 below 0.5 must not round up.
1083        assert_eq!(round_half_away_from_zero(0.499_999_999_999_999_94), 0.0);
1084        // Values at or above 2^52 are already integral and pass through.
1085        let big = 9_007_199_254_740_993f64; // 2^53 + 1 is not representable; nearest is used
1086        assert_eq!(round_half_away_from_zero(big), big);
1087        assert_eq!(
1088            round_half_away_from_zero(4_503_599_627_370_497.0),
1089            4_503_599_627_370_497.0
1090        );
1091        assert!(round_half_away_from_zero(f64::NAN).is_nan());
1092        assert_eq!(round_half_away_from_zero(f64::INFINITY), f64::INFINITY);
1093        assert_eq!(
1094            round_half_away_from_zero(f64::NEG_INFINITY),
1095            f64::NEG_INFINITY
1096        );
1097    }
1098
1099    #[test]
1100    fn test_float_to_int_saturates() {
1101        assert_eq!(u8::from_raster_f64(300.0), 255);
1102        assert_eq!(u8::from_raster_f64(-5.0), 0);
1103        assert_eq!(i8::from_raster_f64(1e9), 127);
1104        assert_eq!(i8::from_raster_f64(-1e9), -128);
1105        assert_eq!(u8::from_raster_f64(f64::NAN), 0);
1106        assert_eq!(i32::from_raster_f64(f64::INFINITY), i32::MAX);
1107        assert_eq!(i32::from_raster_f64(f64::NEG_INFINITY), i32::MIN);
1108    }
1109
1110    #[test]
1111    fn test_int_bridge_is_exact_beyond_f64_mantissa() {
1112        let value = (1i128 << 53) + 1;
1113        assert_eq!(i64::from_raster_i128(value), 9_007_199_254_740_993);
1114        assert_eq!(u64::from_raster_i128(u64::MAX as i128), u64::MAX);
1115        assert_eq!(u64::from_raster_i128(-1), 0);
1116        assert_eq!(i32::from_raster_i128(i128::MAX), i32::MAX);
1117        assert_eq!(i32::from_raster_i128(i128::MIN), i32::MIN);
1118    }
1119
1120    #[test]
1121    fn test_from_ne_slice_truncates_and_pads() {
1122        let bytes = 0x0102_0304u32.to_ne_bytes();
1123        assert_eq!(u32::from_ne_slice(&bytes), 0x0102_0304);
1124        // Extra bytes (complex stride) are ignored.
1125        let mut padded = bytes.to_vec();
1126        padded.extend_from_slice(&[0xFF; 4]);
1127        assert_eq!(u32::from_ne_slice(&padded), 0x0102_0304);
1128        // Short slices yield the default instead of panicking.
1129        assert_eq!(u32::from_ne_slice(&bytes[..2]), 0);
1130    }
1131
1132    #[test]
1133    fn test_elements_as_bytes() {
1134        let values = [0x0102u16, 0x0304u16];
1135        let bytes = elements_as_bytes(&values);
1136        assert_eq!(bytes.len(), 4);
1137        assert_eq!(
1138            bytes,
1139            &[0x0102u16.to_ne_bytes(), 0x0304u16.to_ne_bytes()].concat()[..]
1140        );
1141    }
1142
1143    #[test]
1144    fn test_complex_source_reads_real_component() {
1145        // CFloat32 pixel = [real f32, imaginary f32]
1146        let mut raw = Vec::new();
1147        raw.extend_from_slice(&1.5f32.to_ne_bytes());
1148        raw.extend_from_slice(&9.0f32.to_ne_bytes());
1149        raw.extend_from_slice(&(-2.5f32).to_ne_bytes());
1150        raw.extend_from_slice(&7.0f32.to_ne_bytes());
1151
1152        let mut dst = [0.0f64; 2];
1153        convert_raw_into(&raw, RasterDataType::CFloat32, &mut dst).expect("convert");
1154        assert_eq!(dst, [1.5, -2.5]);
1155    }
1156
1157    #[test]
1158    fn test_complex_destination_zeroes_imaginary() {
1159        let src: Vec<u8> = [1.5f32, -2.5]
1160            .iter()
1161            .flat_map(|v| v.to_ne_bytes())
1162            .collect();
1163        let mut dst = vec![0xAAu8; 16];
1164        convert_raw_bytes(
1165            &src,
1166            RasterDataType::Float32,
1167            &mut dst,
1168            RasterDataType::CFloat32,
1169            FloatToIntRounding::Nearest,
1170        )
1171        .expect("convert");
1172
1173        assert_eq!(&dst[0..4], &1.5f32.to_ne_bytes());
1174        assert_eq!(&dst[4..8], &0f32.to_ne_bytes());
1175        assert_eq!(&dst[8..12], &(-2.5f32).to_ne_bytes());
1176        assert_eq!(&dst[12..16], &0f32.to_ne_bytes());
1177    }
1178
1179    #[test]
1180    fn test_length_validation() {
1181        let src = [0u8; 6];
1182        let mut dst = [0u16; 3];
1183        // 6 bytes / 4-byte samples is not a whole number of samples.
1184        assert!(convert_raw_into(&src, RasterDataType::Float32, &mut dst).is_err());
1185        // 6 bytes / 2-byte samples = 3 samples, matching dst.
1186        assert!(convert_raw_into(&src, RasterDataType::UInt16, &mut dst).is_ok());
1187        // Mismatched destination length.
1188        let mut short = [0u16; 2];
1189        assert!(convert_raw_into(&src, RasterDataType::UInt16, &mut short).is_err());
1190    }
1191
1192    #[test]
1193    fn test_unaligned_source_slice() {
1194        // Deliberately start the sample data at an odd offset so the source can
1195        // never be aligned for f64.
1196        let mut raw = vec![0u8; 1];
1197        for value in [1.0f64, -2.0, 3.5] {
1198            raw.extend_from_slice(&value.to_ne_bytes());
1199        }
1200        let mut dst = [0.0f32; 3];
1201        convert_raw_into(&raw[1..], RasterDataType::Float64, &mut dst).expect("convert");
1202        assert_eq!(dst, [1.0f32, -2.0, 3.5]);
1203    }
1204
1205    #[test]
1206    fn test_memcpy_fast_path_is_bit_exact() {
1207        let values = [f32::NAN, f32::INFINITY, -0.0, 1.25];
1208        let raw: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
1209        let mut dst = [0.0f32; 4];
1210        convert_raw_into(&raw, RasterDataType::Float32, &mut dst).expect("convert");
1211        assert!(dst[0].is_nan());
1212        assert_eq!(dst[1], f32::INFINITY);
1213        assert_eq!(dst[2].to_bits(), (-0.0f32).to_bits());
1214        assert_eq!(dst[3], 1.25);
1215    }
1216
1217    #[test]
1218    fn test_empty_input() {
1219        let mut dst: [f64; 0] = [];
1220        convert_raw_into(&[], RasterDataType::UInt16, &mut dst).expect("convert");
1221        convert_raw_into(&[], RasterDataType::Float64, &mut dst).expect("memcpy path");
1222    }
1223
1224    // -- bulk fast paths vs the per-sample reference ------------------------
1225
1226    /// The conversion kernel exactly as it was before the bulk paths existed:
1227    /// one sample assembled from its byte chunk, no slice reinterpretation and
1228    /// no typed loads.
1229    ///
1230    /// Every fast path must agree with this **bit for bit**, which is what
1231    /// [`assert_paths_agree`] checks — on NaN payloads, `-0.0`, denormals and
1232    /// every integer bound, with the source and/or the destination deliberately
1233    /// misaligned.
1234    fn reference_convert<S: RasterElement, D: RasterElement>(
1235        src: &[u8],
1236        dst: &mut [D],
1237        rounding: FloatToIntRounding,
1238    ) {
1239        for (chunk, out) in src.chunks_exact(S::SIZE).zip(dst.iter_mut()) {
1240            let value = S::from_ne_slice(chunk);
1241            *out = match (S::KIND, D::KIND, rounding) {
1242                (RasterElementKind::Integer, RasterElementKind::Integer, _) => {
1243                    D::from_raster_i128(value.to_raster_i128())
1244                }
1245                (_, _, FloatToIntRounding::Nearest) => D::from_raster_f64(value.to_raster_f64()),
1246                (_, _, FloatToIntRounding::Truncate) => {
1247                    D::from_raster_f64_truncating(value.to_raster_f64())
1248                }
1249            };
1250        }
1251    }
1252
1253    /// Asserts two conversion results are the same, bit for bit.
1254    ///
1255    /// The one admitted exception is a `NaN` **payload** under Miri. Rust does
1256    /// not specify which payload a float cast produces, and Miri deliberately
1257    /// varies it between otherwise identical operations precisely to catch code
1258    /// that relies on one. On a real target both paths run the same instruction
1259    /// on the same input and the payloads do match, which is what the strict
1260    /// comparison below pins down everywhere except under the interpreter. The
1261    /// exception is narrow on purpose: it applies only when *both* sides are
1262    /// `NaN`, and never to an integer destination (where `NaN` maps to `0`
1263    /// deterministically).
1264    fn assert_same_bits<D: RasterElement>(actual: &[D], expected: &[D], label: &str) {
1265        if cfg!(miri)
1266            && D::KIND == RasterElementKind::Float
1267            && actual.len() == expected.len()
1268            && actual.iter().zip(expected.iter()).all(|(a, b)| {
1269                let (a, b) = (a.to_raster_f64(), b.to_raster_f64());
1270                a.to_bits() == b.to_bits() || (a.is_nan() && b.is_nan())
1271            })
1272        {
1273            return;
1274        }
1275        assert_eq!(
1276            elements_as_bytes(actual),
1277            elements_as_bytes(expected),
1278            "{label}"
1279        );
1280    }
1281
1282    /// Runs every code path for `S → D` over `values` and asserts they produce
1283    /// byte-identical output.
1284    ///
1285    /// Paths exercised: the reference loop, the typed entry point with an
1286    /// aligned source, the same with a source deliberately offset by one byte
1287    /// (unaligned path), and the byte entry point with an aligned and with a
1288    /// misaligned destination.
1289    fn assert_paths_agree<S: RasterElement, D: RasterElement>(
1290        values: &[S],
1291        rounding: FloatToIntRounding,
1292    ) {
1293        let count = values.len();
1294        let src = elements_as_bytes(values);
1295
1296        let mut expected = vec![D::default(); count];
1297        reference_convert::<S, D>(src, &mut expected, rounding);
1298
1299        let label = |path: &str| {
1300            format!(
1301                "{:?} -> {:?} ({:?}) mismatch on the {path} path",
1302                S::DATA_TYPE,
1303                D::DATA_TYPE,
1304                rounding
1305            )
1306        };
1307
1308        // Typed destination, aligned source.
1309        let mut typed = vec![D::default(); count];
1310        convert_raw_into_with(src, S::DATA_TYPE, &mut typed, rounding).expect("typed convert");
1311        assert_same_bits(&typed, &expected, &label("typed"));
1312
1313        // Typed destination, source shifted one byte out of alignment.
1314        let mut shifted = Vec::with_capacity(src.len() + 1);
1315        shifted.push(0xA5u8);
1316        shifted.extend_from_slice(src);
1317        let mut unaligned = vec![D::default(); count];
1318        convert_raw_into_with(&shifted[1..], S::DATA_TYPE, &mut unaligned, rounding)
1319            .expect("unaligned convert");
1320        assert_same_bits(&unaligned, &expected, &label("unaligned-source"));
1321
1322        // Byte destination, both sides aligned.
1323        let mut bytes_out = vec![D::default(); count];
1324        convert_raw_bytes(
1325            src,
1326            S::DATA_TYPE,
1327            byte_view(&mut bytes_out),
1328            D::DATA_TYPE,
1329            rounding,
1330        )
1331        .expect("byte convert");
1332        assert_same_bits(&bytes_out, &expected, &label("bytes"));
1333
1334        // Byte destination, destination shifted one byte out of alignment.
1335        let mut shifted_out = vec![0u8; count * D::SIZE + 1];
1336        convert_raw_bytes(
1337            &shifted[1..],
1338            S::DATA_TYPE,
1339            &mut shifted_out[1..],
1340            D::DATA_TYPE,
1341            rounding,
1342        )
1343        .expect("byte convert (misaligned)");
1344        // Decode the written bytes back with `from_ne_bytes` — a pure reinterpret
1345        // with no arithmetic in it, so this compares exactly what was stored.
1346        let mut recovered = vec![D::default(); count];
1347        for (slot, chunk) in recovered
1348            .iter_mut()
1349            .zip(shifted_out[1..].chunks_exact(D::SIZE))
1350        {
1351            *slot = D::from_ne_slice(chunk);
1352        }
1353        assert_same_bits(&recovered, &expected, &label("unaligned-destination"));
1354    }
1355
1356    /// The native-endian byte image of a typed buffer, as a unique borrow.
1357    fn byte_view<D: RasterElement>(values: &mut [D]) -> &mut [u8] {
1358        // SAFETY: identical to `elements_as_bytes`, for a unique borrow: `D` is
1359        // a primitive numeric (sealed trait) with no padding and no invalid bit
1360        // patterns, so every byte is initialised and any byte value written back
1361        // is a valid `D`. `u8` needs no alignment and the slice covers exactly
1362        // the same allocation.
1363        unsafe {
1364            core::slice::from_raw_parts_mut(
1365                values.as_mut_ptr().cast::<u8>(),
1366                core::mem::size_of_val(values),
1367            )
1368        }
1369    }
1370
1371    /// Repeats `values` to a length that is not a multiple of any plausible
1372    /// vector width, so the vectorised body *and* its scalar tail are covered.
1373    fn padded<S: RasterElement>(values: &[S]) -> Vec<S> {
1374        // Miri interprets every one of these conversions, and the matrix covers
1375        // 100 type pairs under two rounding modes; a shorter run still crosses
1376        // the vector body and its tail, which is all the UB checks need.
1377        const LENGTH: usize = if cfg!(miri) { 37 } else { 293 };
1378        let mut out = Vec::with_capacity(LENGTH);
1379        while out.len() < LENGTH {
1380            out.extend_from_slice(values);
1381        }
1382        out.truncate(LENGTH);
1383        out
1384    }
1385
1386    /// Cross-checks one source type against all ten destination types, under
1387    /// both rounding modes.
1388    fn check_all_destinations<S: RasterElement>(values: &[S]) {
1389        let values = padded(values);
1390        for rounding in [FloatToIntRounding::Nearest, FloatToIntRounding::Truncate] {
1391            assert_paths_agree::<S, u8>(&values, rounding);
1392            assert_paths_agree::<S, i8>(&values, rounding);
1393            assert_paths_agree::<S, u16>(&values, rounding);
1394            assert_paths_agree::<S, i16>(&values, rounding);
1395            assert_paths_agree::<S, u32>(&values, rounding);
1396            assert_paths_agree::<S, i32>(&values, rounding);
1397            assert_paths_agree::<S, u64>(&values, rounding);
1398            assert_paths_agree::<S, i64>(&values, rounding);
1399            assert_paths_agree::<S, f32>(&values, rounding);
1400            assert_paths_agree::<S, f64>(&values, rounding);
1401        }
1402    }
1403
1404    #[test]
1405    fn test_bulk_paths_match_reference_for_integer_sources() {
1406        check_all_destinations::<u8>(&[0, 1, 127, 128, 254, 255]);
1407        check_all_destinations::<i8>(&[i8::MIN, -128 + 1, -1, 0, 1, 126, i8::MAX]);
1408        check_all_destinations::<u16>(&[0, 1, 255, 256, 32_767, 32_768, 65_534, u16::MAX]);
1409        check_all_destinations::<i16>(&[i16::MIN, -32_767, -1, 0, 1, 255, 256, i16::MAX]);
1410        check_all_destinations::<u32>(&[0, 1, 65_535, 65_536, 2_147_483_647, u32::MAX]);
1411        check_all_destinations::<i32>(&[i32::MIN, -1, 0, 1, 16_777_217, i32::MAX]);
1412        check_all_destinations::<u64>(&[0, 1, (1 << 53) + 1, u64::MAX - 1, u64::MAX]);
1413        check_all_destinations::<i64>(&[i64::MIN, -(1 << 53) - 1, -1, 0, (1 << 53) + 1, i64::MAX]);
1414    }
1415
1416    #[test]
1417    fn test_bulk_paths_match_reference_for_float_sources() {
1418        // NaN with a non-default payload, both signs of zero, subnormals, the
1419        // integer-bound neighbourhood and the halfway cases the rounding modes
1420        // disagree on.
1421        let f32_values = [
1422            f32::from_bits(0x7FC0_1234),
1423            f32::from_bits(0xFFC0_0001),
1424            f32::NAN,
1425            f32::INFINITY,
1426            f32::NEG_INFINITY,
1427            0.0,
1428            -0.0,
1429            f32::MIN_POSITIVE,
1430            f32::from_bits(1),  // smallest subnormal
1431            -f32::from_bits(1), // negative subnormal
1432            2.5,
1433            -2.5,
1434            0.499_999_97,
1435            255.5,
1436            -0.5,
1437            65_535.5,
1438            f32::MAX,
1439            f32::MIN,
1440            16_777_217.0,
1441            -16_777_217.0,
1442        ];
1443        check_all_destinations::<f32>(&f32_values);
1444
1445        let f64_values = [
1446            f64::from_bits(0x7FF8_0000_1234_5678),
1447            f64::from_bits(0xFFF8_0000_0000_0001),
1448            f64::NAN,
1449            f64::INFINITY,
1450            f64::NEG_INFINITY,
1451            0.0,
1452            -0.0,
1453            f64::MIN_POSITIVE,
1454            f64::from_bits(1),
1455            -f64::from_bits(1),
1456            2.5,
1457            -2.5,
1458            0.499_999_999_999_999_94,
1459            255.5,
1460            -0.5,
1461            4_294_967_295.5,
1462            9_007_199_254_740_993.0,
1463            f64::MAX,
1464            f64::MIN,
1465            1e300,
1466        ];
1467        check_all_destinations::<f64>(&f64_values);
1468    }
1469
1470    #[test]
1471    fn test_misaligned_source_takes_the_unaligned_path() {
1472        // Guards the assumption the bit-identity test relies on: a `Vec<u8>`
1473        // offset by one byte really is rejected by the aligned reinterpretation
1474        // (for every multi-byte type), so the unaligned path is genuinely
1475        // covered rather than silently skipped.
1476        // Heap-allocated on purpose: the allocator hands back a well-aligned
1477        // block, so `&raw[1..]` is reliably *mis*aligned for every wider type.
1478        let raw: Vec<u8> = (0u8..64).collect();
1479        assert!(
1480            samples_from_bytes::<u8>(&raw[1..]).is_some(),
1481            "u8 is align 1"
1482        );
1483        assert!(samples_from_bytes::<u16>(&raw[1..]).is_none());
1484        assert!(samples_from_bytes::<f32>(&raw[1..]).is_none());
1485        assert!(samples_from_bytes::<f64>(&raw[1..]).is_none());
1486        // A length that is not a whole number of samples is rejected too.
1487        assert!(samples_from_bytes::<f64>(&raw[..12]).is_none());
1488    }
1489
1490    #[test]
1491    fn test_unaligned_load_decodes_exactly_like_from_ne_bytes() {
1492        // The unaligned path swaps `S::from_ne_bytes` for a typed unaligned
1493        // read. This pins the claim that the two are the same decode, bit for
1494        // bit — the byte patterns below are a NaN with a payload, a negative
1495        // zero, a subnormal and a negative infinity.
1496        let values = [
1497            f32::from_bits(0x7FC0_1234),
1498            -0.0,
1499            f32::from_bits(1),
1500            1.25,
1501            f32::NEG_INFINITY,
1502        ];
1503        let mut shifted = vec![0xA5u8];
1504        shifted.extend_from_slice(elements_as_bytes(&values));
1505
1506        // Run the very same bytes through the *integer* instantiation: `u32` is
1507        // exact end to end (no float cast is involved anywhere), so this asserts
1508        // the load itself is bit-exact — including under Miri, which is free to
1509        // give a float cast a different NaN payload each time.
1510        let expected_bits: Vec<u32> = values.iter().map(|v| v.to_bits()).collect();
1511        let mut as_bits = [0u32; 5];
1512        convert_unaligned_samples::<u32, u32>(
1513            &shifted[1..],
1514            &mut as_bits,
1515            FloatToIntRounding::Nearest,
1516        );
1517        assert_eq!(as_bits.as_slice(), expected_bits.as_slice());
1518
1519        let mut bytes_out = vec![0u8; values.len() * 4 + 1];
1520        convert_unaligned_bytes::<u32, u32>(
1521            &shifted[1..],
1522            &mut bytes_out[1..],
1523            FloatToIntRounding::Nearest,
1524        );
1525        assert_eq!(&bytes_out[1..], elements_as_bytes(&values));
1526
1527        // And once as `f32`, where the value must survive the `f64` round trip
1528        // the conversion performs (payloads excepted under Miri, see
1529        // `assert_same_bits`).
1530        let mut decoded = [0.0f32; 5];
1531        convert_unaligned_samples::<f32, f32>(
1532            &shifted[1..],
1533            &mut decoded,
1534            FloatToIntRounding::Nearest,
1535        );
1536        assert_same_bits(&decoded, &values, "unaligned f32 decode");
1537    }
1538
1539    #[test]
1540    fn test_sample_capacity_never_divides_by_zero() {
1541        assert_eq!(sample_capacity(9, 4), 2);
1542        assert_eq!(sample_capacity(0, 4), 0);
1543        assert_eq!(sample_capacity(9, 0), 0);
1544    }
1545}