pub const MAX_PREDICTOR: u8 = 7;
pub const MAX_DIFFERENCE_CATEGORY: u8 = 16;
pub const DIFFERENCE_SYMBOLS: usize = MAX_DIFFERENCE_CATEGORY as usize + 1;
pub const MAX_DIFFERENCE_BITS: u32 = 31;
#[inline]
#[must_use]
pub const fn predict(predictor: u8, ra: u16, rb: u16, rc: u16) -> u16 {
let (a, b, c) = (ra as i32, rb as i32, rc as i32);
let prediction = match predictor {
1 => a,
2 => b,
3 => c,
4 => a + b - c,
5 => a + ((b - c) >> 1),
6 => b + ((a - c) >> 1),
_ => (a + b) >> 1,
};
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "predictions are reduced modulo 2^16"
)]
let wrapped = prediction as u16;
wrapped
}
#[inline]
#[must_use]
pub const fn predict_at(
predictor: u8,
initial_predictor: u16,
x: usize,
row: usize,
[ra, rb, rc]: [u16; 3],
) -> u16 {
match (row, x) {
(0, 0) => initial_predictor,
(0, _) => ra,
(_, 0) => rb,
_ => predict(predictor, ra, rb, rc),
}
}
#[inline]
#[must_use]
pub const fn initial_predictor(precision: u8, point_transform: u8) -> Option<u16> {
if precision > 16 || point_transform >= precision {
return None;
}
Some(1 << (precision - point_transform - 1))
}
#[inline]
#[must_use]
pub const fn difference_category(difference: u16) -> (u8, u16) {
#[expect(
clippy::cast_possible_wrap,
reason = "differences are read as two's-complement values modulo 2^16"
)]
let signed = difference as i16;
if signed == i16::MIN {
return (MAX_DIFFERENCE_CATEGORY, 0);
}
let magnitude = signed.unsigned_abs();
#[expect(
clippy::cast_possible_truncation,
reason = "a u16 bit length is at most 16"
)]
let category = (u16::BITS - magnitude.leading_zeros()) as u8;
let bits = if signed >= 0 {
magnitude
} else {
difference.wrapping_sub(1) & ((1 << category) - 1)
};
(category, bits)
}
#[inline]
#[must_use]
pub const fn additional_bits(category: u8) -> u8 {
if category >= MAX_DIFFERENCE_CATEGORY {
0
} else {
category
}
}
#[cfg(test)]
mod tests {
use super::*;
fn decode_difference(category: u8, bits: u16) -> u16 {
match category {
0 => 0,
MAX_DIFFERENCE_CATEGORY => 32768,
_ if bits < 1 << (category - 1) => bits.wrapping_sub((1 << category) - 1),
_ => bits,
}
}
#[test]
fn every_difference_round_trips_through_its_category() {
for difference in 0..=u16::MAX {
let (category, bits) = difference_category(difference);
assert!(category <= MAX_DIFFERENCE_CATEGORY);
assert!(u32::from(bits) < 1 << additional_bits(category));
assert_eq!(
decode_difference(category, bits),
difference,
"{difference}"
);
}
}
#[test]
fn categories_follow_the_magnitude() {
assert_eq!(difference_category(0), (0, 0));
assert_eq!(difference_category(1), (1, 1));
assert_eq!(difference_category(u16::MAX), (1, 0));
assert_eq!(difference_category(255), (8, 255));
assert_eq!(difference_category(0u16.wrapping_sub(255)), (8, 0));
assert_eq!(difference_category(32767), (15, 32767));
assert_eq!(difference_category(32768), (16, 0));
assert_eq!(difference_category(32769), (15, 0));
}
#[test]
fn predictors_match_t81_table_h1_modulo_2_16() {
let (ra, rb, rc) = (1000_u16, 3000_u16, 500_u16);
assert_eq!(predict(1, ra, rb, rc), 1000);
assert_eq!(predict(2, ra, rb, rc), 3000);
assert_eq!(predict(3, ra, rb, rc), 500);
assert_eq!(predict(4, ra, rb, rc), 3500);
assert_eq!(predict(5, ra, rb, rc), 2250);
assert_eq!(predict(6, ra, rb, rc), 3250);
assert_eq!(predict(7, ra, rb, rc), 2000);
assert_eq!(predict(5, 0, 0, 3), 0u16.wrapping_sub(2));
assert_eq!(predict(6, 0, 0, 3), 0u16.wrapping_sub(2));
assert_eq!(predict(4, u16::MAX, u16::MAX, 0), u16::MAX - 1);
assert_eq!(predict(7, u16::MAX, u16::MAX, 0), u16::MAX);
}
#[test]
fn edges_use_the_one_dimensional_predictors() {
let samples = [10, 20, 30];
assert_eq!(predict_at(7, 512, 0, 0, samples), 512);
assert_eq!(predict_at(7, 512, 3, 0, samples), 10);
assert_eq!(predict_at(7, 512, 0, 4, samples), 20);
assert_eq!(predict_at(7, 512, 3, 4, samples), 15);
}
#[test]
fn the_seed_is_half_the_transformed_range() {
assert_eq!(initial_predictor(16, 0), Some(32768));
assert_eq!(initial_predictor(12, 2), Some(512));
assert_eq!(initial_predictor(2, 1), Some(1));
assert_eq!(initial_predictor(8, 8), None);
assert_eq!(initial_predictor(17, 0), None);
}
}