vyre_libs/visual/downsample/mod.rs
1//! 2× box-filter downsample for half-resolution blur.
2//!
3//! Averages each 2×2 block of pixels into one output pixel.
4//! Category A composition - pure IR. No Tier 2.5 primitives.
5
6use vyre::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
7
8const OP_ID: &str = "vyre-libs::visual::downsample";
9
10/// Build a Program that 2× downsamples `input` into `output`.
11///
12/// - `input`: `[u32; width * height]` - source pixels (packed RGBA)
13/// - `output`: `[u32; (width/2) * (height/2)]` - downsampled result
14/// - Width and height must be even.
15#[must_use]
16pub fn downsample_2x(input: &str, output: &str, width: u32, height: u32) -> Program {
17 let out_w = width / 2;
18 let out_h = height / 2;
19 let input_count = width.saturating_mul(height);
20 let output_count = out_w.saturating_mul(out_h);
21
22 Program::wrapped(
23 vec![
24 BufferDecl::storage(input, 0, BufferAccess::ReadOnly, DataType::U32)
25 .with_count(input_count),
26 BufferDecl::storage(output, 1, BufferAccess::ReadWrite, DataType::U32)
27 .with_count(output_count),
28 ],
29 super::PIXEL_WORKGROUP_SIZE,
30 vec![crate::region::wrap_anonymous(
31 OP_ID,
32 vec![
33 Node::let_bind("idx", Expr::gid_x()),
34 Node::if_then(
35 Expr::lt(Expr::var("idx"), Expr::u32(output_count)),
36 vec![
37 Node::let_bind("ox", Expr::rem(Expr::var("idx"), Expr::u32(out_w.max(1)))),
38 Node::let_bind("oy", Expr::div(Expr::var("idx"), Expr::u32(out_w.max(1)))),
39 // Source pixel coordinates.
40 Node::let_bind("sx", Expr::mul(Expr::var("ox"), Expr::u32(2))),
41 Node::let_bind("sy", Expr::mul(Expr::var("oy"), Expr::u32(2))),
42 // Load 4 source pixels.
43 Node::let_bind(
44 "p00",
45 Expr::load(
46 input,
47 Expr::add(
48 Expr::mul(Expr::var("sy"), Expr::u32(width)),
49 Expr::var("sx"),
50 ),
51 ),
52 ),
53 Node::let_bind(
54 "p10",
55 Expr::load(
56 input,
57 Expr::add(
58 Expr::mul(Expr::var("sy"), Expr::u32(width)),
59 Expr::add(Expr::var("sx"), Expr::u32(1)),
60 ),
61 ),
62 ),
63 Node::let_bind(
64 "p01",
65 Expr::load(
66 input,
67 Expr::add(
68 Expr::mul(
69 Expr::add(Expr::var("sy"), Expr::u32(1)),
70 Expr::u32(width),
71 ),
72 Expr::var("sx"),
73 ),
74 ),
75 ),
76 Node::let_bind(
77 "p11",
78 Expr::load(
79 input,
80 Expr::add(
81 Expr::mul(
82 Expr::add(Expr::var("sy"), Expr::u32(1)),
83 Expr::u32(width),
84 ),
85 Expr::add(Expr::var("sx"), Expr::u32(1)),
86 ),
87 ),
88 ),
89 // Average each channel: (c0+c1+c2+c3+2) >> 2
90 // R channel
91 Node::let_bind(
92 "r",
93 Expr::shr(
94 Expr::add(
95 Expr::add(
96 Expr::add(
97 Expr::bitand(Expr::var("p00"), Expr::u32(0xFF)),
98 Expr::bitand(Expr::var("p10"), Expr::u32(0xFF)),
99 ),
100 Expr::add(
101 Expr::bitand(Expr::var("p01"), Expr::u32(0xFF)),
102 Expr::bitand(Expr::var("p11"), Expr::u32(0xFF)),
103 ),
104 ),
105 Expr::u32(2),
106 ),
107 Expr::u32(2),
108 ),
109 ),
110 // G channel
111 Node::let_bind(
112 "g",
113 Expr::shr(
114 Expr::add(
115 Expr::add(
116 Expr::add(
117 Expr::bitand(
118 Expr::shr(Expr::var("p00"), Expr::u32(8)),
119 Expr::u32(0xFF),
120 ),
121 Expr::bitand(
122 Expr::shr(Expr::var("p10"), Expr::u32(8)),
123 Expr::u32(0xFF),
124 ),
125 ),
126 Expr::add(
127 Expr::bitand(
128 Expr::shr(Expr::var("p01"), Expr::u32(8)),
129 Expr::u32(0xFF),
130 ),
131 Expr::bitand(
132 Expr::shr(Expr::var("p11"), Expr::u32(8)),
133 Expr::u32(0xFF),
134 ),
135 ),
136 ),
137 Expr::u32(2),
138 ),
139 Expr::u32(2),
140 ),
141 ),
142 // B channel
143 Node::let_bind(
144 "b",
145 Expr::shr(
146 Expr::add(
147 Expr::add(
148 Expr::add(
149 Expr::bitand(
150 Expr::shr(Expr::var("p00"), Expr::u32(16)),
151 Expr::u32(0xFF),
152 ),
153 Expr::bitand(
154 Expr::shr(Expr::var("p10"), Expr::u32(16)),
155 Expr::u32(0xFF),
156 ),
157 ),
158 Expr::add(
159 Expr::bitand(
160 Expr::shr(Expr::var("p01"), Expr::u32(16)),
161 Expr::u32(0xFF),
162 ),
163 Expr::bitand(
164 Expr::shr(Expr::var("p11"), Expr::u32(16)),
165 Expr::u32(0xFF),
166 ),
167 ),
168 ),
169 Expr::u32(2),
170 ),
171 Expr::u32(2),
172 ),
173 ),
174 // A channel
175 Node::let_bind(
176 "a",
177 Expr::shr(
178 Expr::add(
179 Expr::add(
180 Expr::add(
181 Expr::shr(Expr::var("p00"), Expr::u32(24)),
182 Expr::shr(Expr::var("p10"), Expr::u32(24)),
183 ),
184 Expr::add(
185 Expr::shr(Expr::var("p01"), Expr::u32(24)),
186 Expr::shr(Expr::var("p11"), Expr::u32(24)),
187 ),
188 ),
189 Expr::u32(2),
190 ),
191 Expr::u32(2),
192 ),
193 ),
194 // Pack RGBA.
195 Node::let_bind(
196 "packed",
197 Expr::bitor(
198 Expr::bitor(
199 Expr::var("r"),
200 Expr::shl(Expr::var("g"), Expr::u32(8)),
201 ),
202 Expr::bitor(
203 Expr::shl(Expr::var("b"), Expr::u32(16)),
204 Expr::shl(Expr::var("a"), Expr::u32(24)),
205 ),
206 ),
207 ),
208 // Write output.
209 Node::let_bind(
210 "oidx",
211 Expr::add(
212 Expr::mul(Expr::var("oy"), Expr::u32(out_w)),
213 Expr::var("ox"),
214 ),
215 ),
216 Node::store(output, Expr::var("oidx"), Expr::var("packed")),
217 ],
218 ),
219 ],
220 )],
221 )
222}
223
224inventory::submit! {
225 crate::harness::OpEntry {
226 id: OP_ID,
227 build: || downsample_2x("input", "output", 4, 4),
228 test_inputs: Some(|| {
229 // 4×4 all-white → 2×2 all-white
230 let input = vec![0xFFFF_FFFFu32; 16];
231 vec![vec![
232 crate::visual::byte_helpers::u32_words_to_le_bytes(&input),
233 vec![0u8; 16],
234 ]]
235 }),
236 expected_output: Some(|| {
237 let expected = vec![0xFFFF_FFFFu32; 4];
238 vec![vec![crate::visual::byte_helpers::u32_words_to_le_bytes(&expected)]]
239 }),
240 category: Some("visual"),
241 }
242}