1use crate::clip::ClipRect;
9use crate::framebuffer::{pack_rgba, unpack, Framebuffer};
10
11#[derive(Clone, Debug)]
17pub struct BayerMatrix {
18 pub matrix: [[u8; 8]; 8],
20}
21
22impl BayerMatrix {
23 pub fn standard_8x8() -> Self {
29 Self {
31 matrix: [
32 [0, 32, 8, 40, 2, 34, 10, 42],
33 [48, 16, 56, 24, 50, 18, 58, 26],
34 [12, 44, 4, 36, 14, 46, 6, 38],
35 [60, 28, 52, 20, 62, 30, 54, 22],
36 [3, 35, 11, 43, 1, 33, 9, 41],
37 [51, 19, 59, 27, 49, 17, 57, 25],
38 [15, 47, 7, 39, 13, 45, 5, 37],
39 [63, 31, 55, 23, 61, 29, 53, 21],
40 ],
41 }
42 }
43
44 pub fn threshold(&self, x: u32, y: u32) -> f32 {
48 let tx = (x % 8) as usize;
49 let ty = (y % 8) as usize;
50 self.matrix[ty][tx] as f32 / 64.0
51 }
52}
53
54impl Default for BayerMatrix {
55 fn default() -> Self {
56 Self::standard_8x8()
57 }
58}
59
60pub fn ordered_dither_rgba(fb: &mut Framebuffer, rect: ClipRect, bits_to_drop: u32) {
68 if bits_to_drop == 0 {
69 return;
70 }
71 let bits = bits_to_drop.min(7);
72 let step = (1u32 << bits) as f32; let matrix = BayerMatrix::standard_8x8();
74
75 let fb_w = fb.width();
76 let fb_h = fb.height();
77 let x0 = rect.x0.max(0) as u32;
78 let y0 = rect.y0.max(0) as u32;
79 let x1 = (rect.x1 as u32).min(fb_w);
80 let y1 = (rect.y1 as u32).min(fb_h);
81
82 for y in y0..y1 {
83 for x in x0..x1 {
84 let Some(px) = fb.get(x, y) else { continue };
85 let (r, g, b, a) = unpack(px);
86 let thresh = matrix.threshold(x, y); let dither_channel = |c: u8| -> u8 {
89 let v = c as f32 + thresh * step;
90 let q = (v / step).floor() * step;
92 q.clamp(0.0, 255.0) as u8
93 };
94 let nr = dither_channel(r);
95 let ng = dither_channel(g);
96 let nb = dither_channel(b);
97 fb.set(x, y, pack_rgba(nr, ng, nb, a));
98 }
99 }
100}
101
102#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::framebuffer::Framebuffer;
110
111 fn grey_fb(w: u32, h: u32, level: u8) -> Framebuffer {
112 use oxiui_core::Color;
113 Framebuffer::with_fill(w, h, Color(level, level, level, 255))
114 }
115
116 #[test]
117 fn bayer_matrix_range() {
118 let m = BayerMatrix::standard_8x8();
119 let mut all_values = std::collections::HashSet::new();
120 for row in &m.matrix {
121 for &v in row {
122 all_values.insert(v);
123 }
124 }
125 assert_eq!(
127 all_values.len(),
128 64,
129 "8x8 Bayer matrix must have 64 unique values"
130 );
131 assert!(!all_values.contains(&64), "max value must be 63");
132 }
133
134 #[test]
135 fn bayer_threshold_range() {
136 let m = BayerMatrix::standard_8x8();
137 for y in 0..8 {
138 for x in 0..8 {
139 let t = m.threshold(x, y);
140 assert!(
141 (0.0..1.0).contains(&t),
142 "threshold {t} out of range at ({x},{y})"
143 );
144 }
145 }
146 }
147
148 #[test]
149 fn bayer_determinism() {
150 use crate::clip::ClipRect;
152 let mut fb1 = grey_fb(8, 8, 100);
153 let mut fb2 = grey_fb(8, 8, 100);
154 let rect = ClipRect::full(8, 8);
155 ordered_dither_rgba(&mut fb1, rect, 2);
156 ordered_dither_rgba(&mut fb2, rect, 2);
157 for y in 0..8 {
158 for x in 0..8 {
159 assert_eq!(fb1.get(x, y), fb2.get(x, y), "mismatch at ({x},{y})");
160 }
161 }
162 }
163
164 #[test]
165 fn zero_bits_noop() {
166 use crate::clip::ClipRect;
167 use oxiui_core::Color;
168 let mut fb = Framebuffer::with_fill(4, 4, Color(123, 45, 67, 200));
169 let original = fb.get(0, 0);
170 ordered_dither_rgba(&mut fb, ClipRect::full(4, 4), 0);
171 assert_eq!(fb.get(0, 0), original, "0 bits_to_drop must be a no-op");
172 }
173
174 #[test]
175 fn dither_changes_some_pixels() {
176 use crate::clip::ClipRect;
180 let original = grey_fb(8, 8, 10);
181 let mut dithered = grey_fb(8, 8, 10);
182 ordered_dither_rgba(&mut dithered, ClipRect::full(8, 8), 4);
183 let mut changed = 0u32;
184 for y in 0..8 {
185 for x in 0..8 {
186 if original.get(x, y) != dithered.get(x, y) {
187 changed += 1;
188 }
189 }
190 }
191 assert!(
192 changed > 0,
193 "dithering should change at least some pixels (level=10, bits=4)"
194 );
195 }
196}