use std::collections::HashMap;
pub(crate) const DV_HASH_SIZES: [usize; 4] = [8, 16, 32, 64];
const DV_HASH_STEPS: [usize; 4] = [2, 2, 4, 8];
const BUCKET_CAP: usize = 32;
const MAX_INDEXED_AREA: usize = 1 << 22;
const FNV_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01B3;
#[inline]
fn fnv_mix(mut hash: u64, v: u16) -> u64 {
hash ^= u64::from(v);
hash = hash.wrapping_mul(FNV_PRIME);
hash
}
#[inline]
fn hash8(plane: &[u16], stride: usize, y: usize, x: usize) -> (u64, bool) {
let first = plane[y * stride + x];
let mut hash = FNV_BASIS;
let mut uniform = true;
for i in 0..8 {
let row = &plane[(y + i) * stride + x..][..8];
for &v in row {
uniform &= v == first;
hash = fnv_mix(hash, v);
}
}
(hash, uniform)
}
#[inline]
fn compose4(q: [(u64, bool); 4]) -> (u64, bool) {
let mut hash = FNV_BASIS;
let mut uniform = true;
for &(qh, qu) in &q {
uniform &= qu && qh == q[0].0;
for b in qh.to_le_bytes() {
hash = fnv_mix(hash, u16::from(b));
}
}
(hash, uniform)
}
pub(crate) fn hash_block_direct(
plane: &[u16],
stride: usize,
y: usize,
x: usize,
size: usize,
) -> (u64, bool) {
if size == 8 {
return hash8(plane, stride, y, x);
}
let h = size / 2;
compose4([
hash_block_direct(plane, stride, y, x, h),
hash_block_direct(plane, stride, y, x + h, h),
hash_block_direct(plane, stride, y + h, x, h),
hash_block_direct(plane, stride, y + h, x + h, h),
])
}
#[inline]
pub(crate) fn dv_hash_size_idx(size: usize) -> Option<usize> {
DV_HASH_SIZES.iter().position(|&s| s == size)
}
#[derive(Default, Clone)]
pub(crate) struct DvHashIndex {
maps: [HashMap<u64, Vec<(u16, u16)>>; 4],
}
impl DvHashIndex {
pub(crate) fn build(plane: &[u16], width: usize, height: usize) -> Self {
let mut out = DvHashIndex::default();
if width < 8 || height < 8 || width * height > MAX_INDEXED_AREA {
return out;
}
let gw = (width - 8) / 2 + 1;
let gh = (height - 8) / 2 + 1;
let mut h8_grid = vec![(0u64, false); gw * gh];
for gy in 0..gh {
for gx in 0..gw {
h8_grid[gy * gw + gx] = hash8(plane, width, gy * 2, gx * 2);
}
}
let composed = |y: usize, x: usize, size: usize| -> (u64, bool) {
fn go(grid: &[(u64, bool)], gw: usize, y: usize, x: usize, size: usize) -> (u64, bool) {
if size == 8 {
return grid[(y / 2) * gw + x / 2];
}
let h = size / 2;
compose4([
go(grid, gw, y, x, h),
go(grid, gw, y, x + h, h),
go(grid, gw, y + h, x, h),
go(grid, gw, y + h, x + h, h),
])
}
go(&h8_grid, gw, y, x, size)
};
for (tier, (&size, &step)) in DV_HASH_SIZES.iter().zip(DV_HASH_STEPS.iter()).enumerate() {
if width < size || height < size {
continue;
}
let mut y = 0usize;
while y + size <= height {
let mut x = 0usize;
while x + size <= width {
let (hash, uniform) = composed(y, x, size);
if !uniform {
let bucket = out.maps[tier].entry(hash).or_default();
if bucket.len() < BUCKET_CAP {
bucket.push((y as u16, x as u16));
}
}
x += step;
}
y += step;
}
}
out
}
#[cfg(test)]
pub(crate) fn is_empty(&self) -> bool {
self.maps.iter().all(HashMap::is_empty)
}
pub(crate) fn candidates(
&self,
hash: u64,
tier: usize,
row0: usize,
col0: usize,
) -> Vec<(i32, i32)> {
let Some(bucket) = self.maps[tier].get(&hash) else {
return Vec::new();
};
let mut out: Vec<(i32, i32)> = bucket
.iter()
.map(|&(sy, sx)| (sy as i32 - row0 as i32, sx as i32 - col0 as i32))
.filter(|&(dr, dc)| (dr, dc) != (0, 0))
.collect();
out.sort_by_key(|&(dr, dc)| dr.unsigned_abs() + dc.unsigned_abs());
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pattern_plane(w: usize, h: usize, seed: u32) -> Vec<u16> {
let mut state = seed | 1;
let mut next = move || {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
state
};
(0..w * h).map(|_| (next() & 0xff) as u16).collect()
}
#[test]
fn composed_equals_direct() {
let (w, h) = (192, 128);
let plane = pattern_plane(w, h, 0x2468);
let idx = DvHashIndex::build(&plane, w, h);
for &(y, x, s) in &[
(0usize, 0usize, 8usize),
(2, 6, 8),
(16, 40, 16),
(10, 22, 16),
(32, 64, 32),
(4, 8, 32),
(0, 128, 64),
(64, 0, 64),
] {
let (hash, uniform) = hash_block_direct(&plane, w, y, x, s);
assert!(!uniform);
let tier = dv_hash_size_idx(s).unwrap();
if y % DV_HASH_STEPS[tier] == 0 && x % DV_HASH_STEPS[tier] == 0 {
let probe_from = (y + 16, x + 16);
let cands = idx.candidates(hash, tier, probe_from.0, probe_from.1);
assert!(
cands.contains(&(-16, -16)),
"index missed its own content at ({y},{x})x{s}"
);
}
}
}
#[test]
fn uniform_suppression() {
let (w, h) = (128, 128);
let mut plane = vec![77u16; w * h];
let (_, uniform) = hash_block_direct(&plane, w, 0, 0, 64);
assert!(uniform);
plane[63 * w + 63] = 78;
let (_, uniform) = hash_block_direct(&plane, w, 0, 0, 64);
assert!(!uniform);
let flat = vec![13u16; w * h];
let idx = DvHashIndex::build(&flat, w, h);
assert!(idx.is_empty());
}
#[test]
fn off_stride_positions_indexed() {
let (w, h) = (256, 128);
let mut plane = vec![0u16; w * h];
let glyph = pattern_plane(8, 8, 0x1357);
for i in 0..8 {
for j in 0..8 {
plane[(6 + i) * w + 26 + j] = glyph[i * 8 + j];
}
}
let idx = DvHashIndex::build(&plane, w, h);
let (hash, uniform) = hash_block_direct(&plane, w, 6, 26, 8);
assert!(!uniform);
let cands = idx.candidates(hash, 0, 96, 200);
assert!(cands.contains(&(6 - 96, 26 - 200)));
}
#[test]
fn bucket_cap_and_nearest_order() {
let (w, h) = (448, 192);
let mut plane = vec![0u16; w * h];
for y in 0..h {
for x in 0..w {
plane[y * w + x] = ((x % 8) * 8 + (y % 8)) as u16;
}
}
let idx = DvHashIndex::build(&plane, w, h);
let (hash, uniform) = hash_block_direct(&plane, w, 0, 0, 8);
assert!(!uniform);
let bucket = idx.maps[0].get(&hash).expect("glyph bucket");
assert_eq!(bucket.len(), BUCKET_CAP);
let cands = idx.candidates(hash, 0, 64, 64);
for pair in cands.windows(2) {
let d0 = pair[0].0.unsigned_abs() + pair[0].1.unsigned_abs();
let d1 = pair[1].0.unsigned_abs() + pair[1].1.unsigned_abs();
assert!(d0 <= d1, "candidates not nearest-first: {cands:?}");
}
}
#[test]
fn area_guard() {
let idx = DvHashIndex::build(&[], 4096, 4096);
assert!(idx.is_empty());
}
}