Skip to main content

kui_core/
mip.rs

1//! An image drawn smaller is drawn from a level the core halves
2//! (`docs/adr/0044-an-image-drawn-smaller-is-drawn-from-a-level.md`,
3//! backlog V6).
4//!
5//! Level 0 is the image; level *n* + 1 is level *n* with each 2×2 block
6//! averaged into one texel, `⌈w/2⌉ × ⌈h/2⌉`, the last row and column of
7//! an odd size repeated. The average is taken in linear light with the
8//! alpha premultiplied, so a level is neither darker than its source nor
9//! fringed where a transparent edge meets colour. Which level a quad
10//! draws is [`level_for`]: the deepest still at least as many texels as
11//! pixels on both axes.
12
13use std::sync::OnceLock;
14
15/// sRGB byte to linear light, `0..=1`.
16fn to_linear() -> &'static [f32; 256] {
17    static T: OnceLock<[f32; 256]> = OnceLock::new();
18    T.get_or_init(|| {
19        std::array::from_fn(|i| {
20            let c = i as f32 / 255.0;
21            if c <= 0.04045 {
22                c / 12.92
23            } else {
24                ((c + 0.055) / 1.055).powf(2.4)
25            }
26        })
27    })
28}
29
30/// Entries in [`to_srgb`]: linear light quantized to 12 bits, fine
31/// enough that every sRGB byte above the darkest few has its own step.
32const ENCODE_STEPS: usize = 4096;
33
34/// Linear light quantized to `ENCODE_STEPS` back to an sRGB byte.
35fn to_srgb() -> &'static [u8; ENCODE_STEPS] {
36    static T: OnceLock<[u8; ENCODE_STEPS]> = OnceLock::new();
37    T.get_or_init(|| {
38        std::array::from_fn(|i| {
39            let l = i as f32 / (ENCODE_STEPS - 1) as f32;
40            let c = if l <= 0.003_130_8 {
41                l * 12.92
42            } else {
43                1.055 * l.powf(1.0 / 2.4) - 0.055
44            };
45            (c * 255.0).round().clamp(0.0, 255.0) as u8
46        })
47    })
48}
49
50/// The size of the level below a `w × h` one.
51pub fn halved(w: u32, h: u32) -> (u32, u32) {
52    (w.div_ceil(2).max(1), h.div_ceil(2).max(1))
53}
54
55/// How many levels below level 0 a `w × h` image has: halving stops at
56/// 1×1.
57pub fn depth(w: u32, h: u32) -> u8 {
58    let mut n = 0;
59    let (mut w, mut h) = (w, h);
60    while w > 1 || h > 1 {
61        (w, h) = halved(w, h);
62        n += 1;
63    }
64    n
65}
66
67/// Level *n* + 1 from level *n*'s straight RGBA, `w × h × 4` bytes: each
68/// 2×2 block averaged in linear light with the alpha premultiplied, the
69/// colour taken back to straight alpha. Returns the new size and pixels.
70pub fn halve(w: u32, h: u32, rgba: &[u8]) -> (u32, u32, Vec<u8>) {
71    debug_assert_eq!(rgba.len(), w as usize * h as usize * 4);
72    let (nw, nh) = halved(w, h);
73    let (w, h) = (w as usize, h as usize);
74    let lin = to_linear();
75    let enc = to_srgb();
76    let scale = (ENCODE_STEPS - 1) as f32;
77    let mut out = vec![0u8; nw as usize * nh as usize * 4];
78    let quarter = scale / 4.0;
79    for y in 0..nh as usize {
80        let rows = [2 * y, (2 * y + 1).min(h - 1)];
81        let (r0, r1) = (
82            &rgba[rows[0] * w * 4..][..w * 4],
83            &rgba[rows[1] * w * 4..][..w * 4],
84        );
85        let out_row = &mut out[y * nw as usize * 4..][..nw as usize * 4];
86        for (x, o) in out_row.as_chunks_mut::<4>().0.iter_mut().enumerate() {
87            let (c0, c1) = (8 * x, (8 * x + 4).min(w * 4 - 4));
88            let block = [
89                &r0[c0..c0 + 4],
90                &r0[c1..c1 + 4],
91                &r1[c0..c0 + 4],
92                &r1[c1..c1 + 4],
93            ];
94            // Opaque, as a photo is: the plain mean, no weights.
95            if block.iter().all(|p| p[3] == 255) {
96                for c in 0..3 {
97                    let sum: f32 = block.iter().map(|p| lin[p[c] as usize]).sum();
98                    o[c] = enc[(sum * quarter) as usize];
99                }
100                o[3] = 255;
101                continue;
102            }
103            let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0f32, 0.0f32, 0u32);
104            for p in block {
105                let pa = p[3] as f32;
106                r += lin[p[0] as usize] * pa;
107                g += lin[p[1] as usize] * pa;
108                b += lin[p[2] as usize] * pa;
109                a += p[3] as u32;
110            }
111            if a > 0 {
112                // Back to straight alpha: the premultiplied sums over the
113                // alpha sum, the weights of the texels that had any.
114                let inv = scale / a as f32;
115                o[0] = enc[(r * inv) as usize];
116                o[1] = enc[(g * inv) as usize];
117                o[2] = enc[(b * inv) as usize];
118                o[3] = ((a + 2) / 4) as u8;
119            }
120        }
121    }
122    (nw, nh, out)
123}
124
125/// The level a quad draws: the deepest whose texels still cover the
126/// pixels on both axes. `texels` is the texel rect `fit` picked at level
127/// 0, `pixels` the drawn size in physical px (any scale the quad is
128/// drawn through multiplied in), `deepest` the chain's last level.
129/// `⌊log₂ r⌋` for `r` the smaller of the two ratios, so a bilinear
130/// sample inside the level minifies by less than two and a level never
131/// magnifies.
132pub fn level_for(texels: (f32, f32), pixels: (f32, f32), deepest: u8) -> u8 {
133    let (tw, th) = texels;
134    let (pw, ph) = pixels;
135    if !(pw > 0.0 && ph > 0.0 && tw > 0.0 && th > 0.0) {
136        return 0;
137    }
138    let r = (tw / pw).min(th / ph);
139    // NaN is not at least 2 either.
140    if r.partial_cmp(&2.0).is_none_or(|o| o.is_lt()) {
141        return 0;
142    }
143    // `r` is at most a texture's side over a sliver of a pixel; the cast
144    // saturates, and `deepest` bounds it.
145    (r.log2().floor() as u32).min(deepest as u32) as u8
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    fn px(c: [u8; 4], n: usize) -> Vec<u8> {
153        c.iter().copied().cycle().take(n * 4).collect()
154    }
155
156    #[test]
157    fn a_flat_image_halves_to_itself() {
158        let (w, h, out) = halve(4, 2, &px([200, 100, 30, 255], 8));
159        assert_eq!((w, h), (2, 1));
160        assert_eq!(out, px([200, 100, 30, 255], 2));
161    }
162
163    /// Black and white averaged in linear light is sRGB 188, not 128: a
164    /// level in gamma space would darken every edge a photo has.
165    #[test]
166    fn the_average_is_in_linear_light() {
167        let rgba = [0, 0, 0, 255, 255, 255, 255, 255].repeat(2);
168        let (_, _, out) = halve(2, 2, &rgba);
169        assert!((187..=189).contains(&out[0]), "{out:?}");
170        assert_eq!(out[3], 255);
171    }
172
173    /// A transparent texel's colour does not bleed into its neighbour's:
174    /// red beside clear is red at half alpha, not dark red.
175    #[test]
176    fn the_alpha_is_premultiplied() {
177        let rgba = [255, 0, 0, 255, 0, 0, 0, 0].repeat(2);
178        let (_, _, out) = halve(2, 2, &rgba);
179        assert_eq!(&out[..3], &[255, 0, 0]);
180        assert_eq!(out[3], 128);
181    }
182
183    /// An odd side repeats its last row and column: a 3×1 image is two
184    /// texels, the second its last texel alone.
185    #[test]
186    fn an_odd_side_repeats_its_edge() {
187        let rgba = [10, 10, 10, 255, 10, 10, 10, 255, 250, 250, 250, 255];
188        let (w, h, out) = halve(3, 1, &rgba);
189        assert_eq!((w, h), (2, 1));
190        assert_eq!(&out[4..], &[250, 250, 250, 255]);
191    }
192
193    #[test]
194    fn the_chain_ends_at_one_texel() {
195        assert_eq!(depth(1, 1), 0);
196        assert_eq!(depth(2, 1), 1);
197        assert_eq!(depth(4032, 3024), 12);
198        assert_eq!(halved(1, 7), (1, 4));
199    }
200
201    #[test]
202    fn the_level_never_magnifies() {
203        // At most 2:1 is level 0.
204        assert_eq!(level_for((1000.0, 1000.0), (600.0, 600.0), 10), 0);
205        assert_eq!(level_for((1000.0, 1000.0), (500.0, 500.0), 10), 1);
206        assert_eq!(level_for((1000.0, 1000.0), (260.0, 260.0), 10), 1);
207        assert_eq!(level_for((1000.0, 1000.0), (250.0, 250.0), 10), 2);
208        // The smaller ratio decides: stretched wide, the width is
209        // barely minified and keeps its texels.
210        assert_eq!(level_for((1000.0, 1000.0), (900.0, 100.0), 10), 0);
211        // Bounded by the chain, and nothing for an empty draw.
212        assert_eq!(level_for((1000.0, 1000.0), (0.5, 0.5), 3), 3);
213        assert_eq!(level_for((1000.0, 1000.0), (0.0, 10.0), 10), 0);
214        assert_eq!(level_for((1000.0, 1000.0), (f32::NAN, 10.0), 10), 0);
215    }
216}