#![forbid(unsafe_code)]
#![warn(missing_docs)]
mod interp;
mod tables;
mod yuv;
pub fn hq2x(src: &[u32], dest: &mut [u32], width: u32, height: u32) {
let w = width as usize;
hq2x_rb(src, w, dest, w * 2, width, height);
}
pub fn hq3x(src: &[u32], dest: &mut [u32], width: u32, height: u32) {
let w = width as usize;
hq3x_rb(src, w, dest, w * 3, width, height);
}
pub fn hq4x(src: &[u32], dest: &mut [u32], width: u32, height: u32) {
let w = width as usize;
hq4x_rb(src, w, dest, w * 4, width, height);
}
pub fn hq2x_rb(
src: &[u32],
src_row_pixels: usize,
dest: &mut [u32],
dest_row_pixels: usize,
width: u32,
height: u32,
) {
scale::<2>(src, src_row_pixels, dest, dest_row_pixels, width, height);
}
pub fn hq3x_rb(
src: &[u32],
src_row_pixels: usize,
dest: &mut [u32],
dest_row_pixels: usize,
width: u32,
height: u32,
) {
scale::<3>(src, src_row_pixels, dest, dest_row_pixels, width, height);
}
pub fn hq4x_rb(
src: &[u32],
src_row_pixels: usize,
dest: &mut [u32],
dest_row_pixels: usize,
width: u32,
height: u32,
) {
scale::<4>(src, src_row_pixels, dest, dest_row_pixels, width, height);
}
fn scale<const SCALE: usize>(
src: &[u32],
src_row_pixels: usize,
dest: &mut [u32],
dest_row_pixels: usize,
width: u32,
height: u32,
) {
let width = width as usize;
let height = height as usize;
if width == 0 || height == 0 {
return;
}
let spl = src_row_pixels;
let dpl = dest_row_pixels;
let src_required = (height - 1)
.checked_mul(spl)
.and_then(|row| row.checked_add(width))
.expect("src too large: required pixel count does not fit in usize");
assert!(
src.len() >= src_required,
"src too small: need {src_required} pixels, got {}",
src.len()
);
let required = (|| {
let row_base = (height - 1).checked_mul(dpl)?.checked_mul(SCALE)?;
let col_base = (width - 1).checked_mul(SCALE)?;
let block_row = (SCALE - 1).checked_mul(dpl)?;
row_base
.checked_add(col_base)?
.checked_add(block_row)?
.checked_add(SCALE)
})()
.expect("dest too large: required pixel count does not fit in usize");
assert!(
dest.len() >= required,
"dest too small: need {required} pixels, got {}",
dest.len()
);
for j in 0..height {
let prevline = if j > 0 { spl } else { 0 };
let nextline = if j < height - 1 { spl } else { 0 };
let row_base = j * spl;
let dest_row = j * dpl * SCALE;
for i in 0..width {
let center = row_base + i;
let up = center - prevline;
let down = center + nextline;
let mut w = [0u32; 10];
w[2] = src[up];
w[5] = src[center];
w[8] = src[down];
if i > 0 {
w[1] = src[up - 1];
w[4] = src[center - 1];
w[7] = src[down - 1];
} else {
w[1] = w[2];
w[4] = w[5];
w[7] = w[8];
}
if i < width - 1 {
w[3] = src[up + 1];
w[6] = src[center + 1];
w[9] = src[down + 1];
} else {
w[3] = w[2];
w[6] = w[5];
w[9] = w[8];
}
let pattern = pattern_of(&w);
let base = dest_row + i * SCALE;
match SCALE {
2 => tables::fill2(pattern, &w, dest, base, dpl),
3 => tables::fill3(pattern, &w, dest, base, dpl),
_ => tables::fill4(pattern, &w, dest, base, dpl),
}
}
}
}
#[inline]
fn pattern_of(w: &[u32; 10]) -> u8 {
let yuv1 = yuv::rgb_to_yuv(w[5]);
let mut pattern = 0u8;
let mut flag = 1u8;
for k in 1..=9 {
if k == 5 {
continue;
}
if w[k] != w[5] {
let yuv2 = yuv::rgb_to_yuv(w[k]);
if yuv::yuv_diff(yuv1, yuv2) {
pattern |= flag;
}
}
flag <<= 1;
}
pattern
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
fn load_grid(name: &str) -> (usize, usize, Vec<u32>) {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/inputs")
.join(format!("{name}.txt"));
let text = fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path:?}: {e}"));
let mut tok = text.split_whitespace();
let w: usize = tok.next().unwrap().parse().unwrap();
let h: usize = tok.next().unwrap().parse().unwrap();
let px: Vec<u32> = tok.map(|t| u32::from_str_radix(t, 16).unwrap()).collect();
assert_eq!(px.len(), w * h, "pixel count mismatch in {name}");
(w, h, px)
}
fn patterns(w: usize, h: usize, px: &[u32]) -> Vec<u8> {
let at = |x: i64, y: i64| -> u32 {
let xc = x.clamp(0, w as i64 - 1) as usize;
let yc = y.clamp(0, h as i64 - 1) as usize;
px[yc * w + xc]
};
let mut out = Vec::with_capacity(w * h);
for y in 0..h as i64 {
for x in 0..w as i64 {
let mut wn = [0u32; 10];
wn[1] = at(x - 1, y - 1);
wn[2] = at(x, y - 1);
wn[3] = at(x + 1, y - 1);
wn[4] = at(x - 1, y);
wn[5] = at(x, y);
wn[6] = at(x + 1, y);
wn[7] = at(x - 1, y + 1);
wn[8] = at(x, y + 1);
wn[9] = at(x + 1, y + 1);
out.push(pattern_of(&wn));
}
}
out
}
#[test]
fn corpus_exercises_every_fill_pattern() {
const CASES: &[&str] = &[
"all_patterns",
"all_patterns_multicolor",
"rng_24x24",
"checkerboard",
"cross",
];
let mut seen = [false; 256];
for name in CASES {
let (w, h, px) = load_grid(name);
for p in patterns(w, h, &px) {
seen[p as usize] = true;
}
}
let missing: Vec<usize> = (0..256).filter(|&i| !seen[i]).collect();
assert!(missing.is_empty(), "patterns never exercised: {missing:?}");
}
#[test]
fn wrapped_destination_preflight_panics_without_writing() {
let src = [0x0012_3456u32];
let mut dst = [0xDEAD_BEEFu32];
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
hq2x_rb(&src, 1, &mut dst, usize::MAX, 1, 1);
}));
assert!(result.is_err(), "oversized destination stride must panic");
assert_eq!(dst, [0xDEAD_BEEF]);
}
}