#[must_use]
#[inline]
#[allow(clippy::cast_possible_truncation)]
pub fn snorm_i8_encode(v: f32) -> i8 {
let clamped = v.clamp(-1.0, 1.0);
(clamped * 127.0).round() as i8
}
#[must_use]
#[inline]
pub fn snorm_i8_decode(q: i8) -> f32 {
(q as f32 / 127.0).clamp(-1.0, 1.0)
}
#[must_use]
#[inline]
#[allow(clippy::cast_possible_truncation)]
pub fn snorm_i16_encode(v: f32) -> i16 {
let clamped = v.clamp(-1.0, 1.0);
(clamped * 32767.0).round() as i16
}
#[must_use]
#[inline]
pub fn snorm_i16_decode(q: i16) -> f32 {
(q as f32 / 32767.0).clamp(-1.0, 1.0)
}
#[must_use]
#[inline]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn unorm_u8_encode(v: f32) -> u8 {
let clamped = v.clamp(0.0, 1.0);
(clamped * 255.0).round() as u8
}
#[must_use]
#[inline]
pub fn unorm_u8_decode(q: u8) -> f32 {
(q as f32 / 255.0).clamp(0.0, 1.0)
}
#[must_use]
#[inline]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn unorm_u16_encode(v: f32) -> u16 {
let clamped = v.clamp(0.0, 1.0);
(clamped * 65535.0).round() as u16
}
#[must_use]
#[inline]
pub fn unorm_u16_decode(q: u16) -> f32 {
(q as f32 / 65535.0).clamp(0.0, 1.0)
}
#[must_use]
pub const fn half_encode(v: f32) -> u16 {
let bits = v.to_bits();
let sign = ((bits >> 16) & 0x8000) as u16;
let exp_biased = (bits >> 23) & 0xFF;
let mantissa = bits & 0x7FFFFF;
if exp_biased == 0xFF {
if mantissa != 0 {
return sign | 0x7E00; }
return sign | 0x7C00; }
if exp_biased == 0 {
return sign;
}
let exp_unbiased = (exp_biased as i32) - 127;
let new_exp = exp_unbiased + 15;
if new_exp >= 31 {
return sign | 0x7C00; }
if new_exp <= 0 {
if new_exp < -10 {
return sign; }
let full = mantissa | 0x800000;
let shift = (14 - new_exp) as u32; let q = full >> shift;
let rem = full & ((1 << shift) - 1);
let half = 1 << (shift - 1);
let round_up = rem > half || (rem == half && (q & 1) == 1);
return sign | ((q + round_up as u32) as u16);
}
let new_exp_u = new_exp as u16;
let new_mantissa = (mantissa >> 13) as u16;
let round_bit = (mantissa >> 12) & 1;
let sticky = (mantissa & 0x0FFF) != 0;
let round_up = round_bit == 1 && (sticky || (new_mantissa & 1) == 1);
let base = sign | (new_exp_u << 10) | new_mantissa;
if round_up {
base + 1
} else {
base
}
}
#[must_use]
pub fn half_decode(bits: u16) -> f32 {
let sign = ((bits & 0x8000) as u32) << 16;
let exp = ((bits >> 10) & 0x1F) as u32;
let mantissa = (bits & 0x03FF) as u32;
if exp == 0 {
if mantissa == 0 {
return f32::from_bits(sign); }
let m = mantissa as f32;
let val = m * (2.0_f32).powi(-24);
return if sign != 0 { -val } else { val };
}
if exp == 31 {
if mantissa == 0 {
return f32::from_bits(sign | 0x7F80_0000); }
return f32::from_bits(sign | 0x7FC0_0000); }
let new_exp = (exp as i32 - 15 + 127) as u32;
let new_mantissa = mantissa << 13;
f32::from_bits(sign | (new_exp << 23) | new_mantissa)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_snorm_i8_boundary() {
assert_eq!(snorm_i8_encode(1.0), 127);
assert_eq!(snorm_i8_encode(-1.0), -127);
assert_eq!(snorm_i8_encode(0.0), 0);
}
#[test]
fn test_snorm_i8_roundtrip() {
for &v in &[-1.0_f32, -0.5, -0.1, 0.0, 0.1, 0.5, 1.0] {
let q = snorm_i8_encode(v);
let back = snorm_i8_decode(q);
assert!(
(v - back).abs() < 0.01,
"roundtrip failed: {v} → {q} → {back}"
);
}
}
#[test]
fn test_snorm_i8_clamp_out_of_range() {
assert_eq!(snorm_i8_encode(2.0), 127);
assert_eq!(snorm_i8_encode(-2.0), -127);
}
#[test]
fn test_snorm_i16_boundary() {
assert_eq!(snorm_i16_encode(1.0), 32767);
assert_eq!(snorm_i16_encode(-1.0), -32767);
assert_eq!(snorm_i16_encode(0.0), 0);
}
#[test]
fn test_snorm_i16_roundtrip_precision() {
for &v in &[-0.9_f32, -0.3, 0.0, 0.3, 0.7, 0.999] {
let q = snorm_i16_encode(v);
let back = snorm_i16_decode(q);
assert!((v - back).abs() < 5e-5, "i16 roundtrip: {v} → {q} → {back}");
}
}
#[test]
fn test_unorm_u8_boundary() {
assert_eq!(unorm_u8_encode(0.0), 0);
assert_eq!(unorm_u8_encode(1.0), 255);
assert_eq!(unorm_u8_encode(0.5), 128); }
#[test]
fn test_unorm_u8_roundtrip() {
for &v in &[0.0_f32, 0.25, 0.5, 0.75, 1.0] {
let q = unorm_u8_encode(v);
let back = unorm_u8_decode(q);
assert!((v - back).abs() < 0.005, "u8 roundtrip: {v} → {q} → {back}");
}
}
#[test]
fn test_unorm_u16_boundary() {
assert_eq!(unorm_u16_encode(0.0), 0);
assert_eq!(unorm_u16_encode(1.0), 65535);
}
#[test]
fn test_unorm_u16_precision() {
for &v in &[0.0_f32, 0.123, 0.5, 0.876543, 1.0] {
let q = unorm_u16_encode(v);
let back = unorm_u16_decode(q);
assert!((v - back).abs() < 2e-5, "u16 roundtrip: {v} → {q} → {back}");
}
}
#[test]
fn test_half_zero() {
assert_eq!(half_encode(0.0), 0);
assert_eq!(half_encode(-0.0), 0x8000);
assert_eq!(half_decode(0), 0.0);
assert!(half_decode(0x8000).is_sign_negative());
}
#[test]
fn test_half_one() {
assert_eq!(half_encode(1.0), 0x3C00);
assert!((half_decode(0x3C00) - 1.0).abs() < 1e-6);
}
#[test]
fn test_half_negative_one() {
assert_eq!(half_encode(-1.0), 0xBC00);
assert!((half_decode(0xBC00) + 1.0).abs() < 1e-6);
}
#[test]
fn test_half_roundtrip_precision() {
for &v in &[
0.5_f32,
1.5,
std::f32::consts::PI,
100.0,
-50.0,
0.0001,
65000.0,
] {
let bits = half_encode(v);
let back = half_decode(bits);
let rel_err = ((v - back) / v).abs();
assert!(
rel_err < 1e-3,
"FP16 rel err too large: {v} → {bits:#x} → {back} (err={rel_err})"
);
}
}
#[test]
fn test_half_overflow_to_infinity() {
let bits = half_encode(1e10);
assert_eq!(bits, 0x7C00); assert!(half_decode(bits).is_infinite());
assert!(half_decode(bits).is_sign_positive());
}
#[test]
fn test_half_negative_overflow() {
let bits = half_encode(-1e10);
assert_eq!(bits, 0xFC00); assert!(half_decode(bits).is_infinite());
assert!(half_decode(bits).is_sign_negative());
}
#[test]
fn test_half_underflow_to_zero() {
let bits = half_encode(1e-30);
assert_eq!(bits, 0);
}
}