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}