1use std::sync::Arc;
15
16use vyre_foundation::ir::model::expr::{GeneratorRef, Ident};
17use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
18
19const OP_ID: &str = "vyre-libs::visual::blur";
20
21#[must_use]
38pub fn gaussian_blur_2pass(
39 input: &str,
40 output: &str,
41 scratch: &str,
42 width: u32,
43 height: u32,
44 radius: u32,
45 sigma: f32,
46) -> GaussianBlurStages {
47 let kernel = GaussianKernel::new(radius, sigma);
48 gaussian_blur_2pass_with_kernel(input, output, scratch, width, height, &kernel)
49}
50
51#[must_use]
58pub fn gaussian_blur_2pass_with_kernel(
59 input: &str,
60 output: &str,
61 scratch: &str,
62 width: u32,
63 height: u32,
64 kernel: &GaussianKernel,
65) -> GaussianBlurStages {
66 GaussianBlurStages {
67 horizontal: gaussian_blur_pass(
68 input,
69 scratch,
70 width,
71 height,
72 kernel.radius(),
73 kernel.weights(),
74 Axis::Horizontal,
75 ),
76 vertical: gaussian_blur_pass(
77 scratch,
78 output,
79 width,
80 height,
81 kernel.radius(),
82 kernel.weights(),
83 Axis::Vertical,
84 ),
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct GaussianKernel {
91 radius: u32,
92 weights: Vec<u32>,
93}
94
95impl GaussianKernel {
96 #[must_use]
98 pub fn new(radius: u32, sigma: f32) -> Self {
99 let clamped = radius.min(vyre_primitives::math::conv1d::MAX_RADIUS);
100 Self {
101 radius: clamped,
102 weights: vyre_primitives::math::conv1d::gaussian_weights(clamped, sigma),
103 }
104 }
105
106 pub fn from_weights(radius: u32, weights: Vec<u32>) -> Result<Self, GaussianKernelError> {
113 let clamped = radius.min(vyre_primitives::math::conv1d::MAX_RADIUS);
114 let expected = (2 * clamped + 1) as usize;
115 if weights.len() != expected {
116 return Err(GaussianKernelError {
117 radius: clamped,
118 expected,
119 actual: weights.len(),
120 });
121 }
122 Ok(Self {
123 radius: clamped,
124 weights,
125 })
126 }
127
128 #[must_use]
130 pub const fn radius(&self) -> u32 {
131 self.radius
132 }
133
134 #[must_use]
136 pub fn weights(&self) -> &[u32] {
137 &self.weights
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct GaussianKernelError {
144 pub radius: u32,
146 pub expected: usize,
148 pub actual: usize,
150}
151
152impl std::fmt::Display for GaussianKernelError {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 write!(
155 f,
156 "invalid Gaussian kernel for radius {}: expected {} weights, got {}. Fix: supply 2 * radius + 1 fixed-point weights.",
157 self.radius, self.expected, self.actual
158 )
159 }
160}
161
162impl std::error::Error for GaussianKernelError {}
163
164#[derive(Debug)]
166pub struct GaussianBlurStages {
167 pub horizontal: Program,
169 pub vertical: Program,
171}
172
173impl GaussianBlurStages {
174 #[must_use]
176 pub const fn stage_count(&self) -> usize {
177 2
178 }
179
180 #[must_use]
182 pub fn programs(&self) -> [&Program; 2] {
183 [&self.horizontal, &self.vertical]
184 }
185}
186
187#[derive(Clone, Copy)]
188enum Axis {
189 Horizontal,
190 Vertical,
191}
192
193fn gaussian_blur_pass(
194 input: &str,
195 output: &str,
196 width: u32,
197 height: u32,
198 radius: u32,
199 weights: &[u32],
200 axis: Axis,
201) -> Program {
202 let clamped = radius.min(vyre_primitives::math::conv1d::MAX_RADIUS);
203 let diameter = 2 * clamped + 1;
204 let count = width.saturating_mul(height);
205 let is_horiz = matches!(axis, Axis::Horizontal);
206 let dim = if is_horiz {
207 width.max(1)
208 } else {
209 height.max(1)
210 };
211 let parent = GeneratorRef {
212 name: OP_ID.to_string(),
213 };
214
215 let blur_pass = Node::Region {
218 generator: Ident::from(vyre_primitives::math::conv1d::OP_ID),
219 source_region: Some(parent),
220 body: Arc::new(vec![
221 Node::let_bind("idx", Expr::gid_x()),
222 Node::if_then(Expr::lt(Expr::var("idx"), Expr::u32(count)), {
223 let mut body = vec![
224 Node::let_bind("px", Expr::rem(Expr::var("idx"), Expr::u32(width.max(1)))),
225 Node::let_bind("py", Expr::div(Expr::var("idx"), Expr::u32(width.max(1)))),
226 Node::let_bind("acc_r", Expr::u32(0)),
228 Node::let_bind("acc_g", Expr::u32(0)),
229 Node::let_bind("acc_b", Expr::u32(0)),
230 Node::let_bind("acc_a", Expr::u32(0)),
231 ];
232
233 for k in 0..diameter {
236 let w_val = weights[k as usize];
237 if w_val == 0 {
238 continue;
239 }
240 let offset = k as i32 - clamped as i32;
242 let sample_coord = if is_horiz {
243 if offset >= 0 {
245 Expr::select(
246 Expr::lt(
247 Expr::add(Expr::var("px"), Expr::u32(offset as u32)),
248 Expr::u32(dim),
249 ),
250 Expr::add(Expr::var("px"), Expr::u32(offset as u32)),
251 Expr::u32(dim - 1),
252 )
253 } else {
254 Expr::select(
255 Expr::ge(Expr::var("px"), Expr::u32((-offset) as u32)),
256 Expr::sub(Expr::var("px"), Expr::u32((-offset) as u32)),
257 Expr::u32(0),
258 )
259 }
260 } else {
261 if offset >= 0 {
263 Expr::select(
264 Expr::lt(
265 Expr::add(Expr::var("py"), Expr::u32(offset as u32)),
266 Expr::u32(dim),
267 ),
268 Expr::add(Expr::var("py"), Expr::u32(offset as u32)),
269 Expr::u32(dim - 1),
270 )
271 } else {
272 Expr::select(
273 Expr::ge(Expr::var("py"), Expr::u32((-offset) as u32)),
274 Expr::sub(Expr::var("py"), Expr::u32((-offset) as u32)),
275 Expr::u32(0),
276 )
277 }
278 };
279
280 let pixel_idx = if is_horiz {
282 Expr::add(Expr::mul(Expr::var("py"), Expr::u32(width)), sample_coord)
283 } else {
284 Expr::add(Expr::mul(sample_coord, Expr::u32(width)), Expr::var("px"))
285 };
286
287 let tap_name = format!("tap_{k}");
288 body.push(Node::let_bind(&tap_name, Expr::load(input, pixel_idx)));
289
290 body.push(Node::assign(
292 "acc_r",
293 Expr::add(
294 Expr::var("acc_r"),
295 Expr::mul(
296 Expr::bitand(Expr::var(&tap_name), Expr::u32(0xFF)),
297 Expr::u32(w_val),
298 ),
299 ),
300 ));
301 body.push(Node::assign(
302 "acc_g",
303 Expr::add(
304 Expr::var("acc_g"),
305 Expr::mul(
306 Expr::bitand(
307 Expr::shr(Expr::var(&tap_name), Expr::u32(8)),
308 Expr::u32(0xFF),
309 ),
310 Expr::u32(w_val),
311 ),
312 ),
313 ));
314 body.push(Node::assign(
315 "acc_b",
316 Expr::add(
317 Expr::var("acc_b"),
318 Expr::mul(
319 Expr::bitand(
320 Expr::shr(Expr::var(&tap_name), Expr::u32(16)),
321 Expr::u32(0xFF),
322 ),
323 Expr::u32(w_val),
324 ),
325 ),
326 ));
327 body.push(Node::assign(
328 "acc_a",
329 Expr::add(
330 Expr::var("acc_a"),
331 Expr::mul(
332 Expr::shr(Expr::var(&tap_name), Expr::u32(24)),
333 Expr::u32(w_val),
334 ),
335 ),
336 ));
337 }
338
339 let shift_clamp = |acc: &str, out: &str| -> Vec<Node> {
341 vec![
342 Node::let_bind(out, Expr::shr(Expr::var(acc), Expr::u32(16))),
343 Node::assign(
344 out,
345 Expr::select(
346 Expr::gt(Expr::var(out), Expr::u32(255)),
347 Expr::u32(255),
348 Expr::var(out),
349 ),
350 ),
351 ]
352 };
353 body.extend(shift_clamp("acc_r", "or"));
354 body.extend(shift_clamp("acc_g", "og"));
355 body.extend(shift_clamp("acc_b", "ob"));
356 body.extend(shift_clamp("acc_a", "oa"));
357
358 body.push(Node::let_bind(
360 "packed",
361 Expr::bitor(
362 Expr::bitor(Expr::var("or"), Expr::shl(Expr::var("og"), Expr::u32(8))),
363 Expr::bitor(
364 Expr::shl(Expr::var("ob"), Expr::u32(16)),
365 Expr::shl(Expr::var("oa"), Expr::u32(24)),
366 ),
367 ),
368 ));
369 body.push(Node::let_bind(
370 "oidx",
371 Expr::add(
372 Expr::mul(Expr::var("py"), Expr::u32(width)),
373 Expr::var("px"),
374 ),
375 ));
376 body.push(Node::store(output, Expr::var("oidx"), Expr::var("packed")));
377 body
378 }),
379 ]),
380 };
381
382 Program::wrapped(
383 vec![
384 BufferDecl::storage(input, 0, BufferAccess::ReadOnly, DataType::U32).with_count(count),
385 BufferDecl::storage(output, 1, BufferAccess::ReadWrite, DataType::U32)
386 .with_count(count),
387 ],
388 super::PIXEL_WORKGROUP_SIZE,
389 vec![crate::region::wrap_anonymous(OP_ID, vec![blur_pass])],
390 )
391}
392
393pub use vyre_primitives::math::conv1d::gaussian_weights;
395
396inventory::submit! {
397 vyre_foundation::operation::OperationRegistration {
398 semantic_version: 1,
399 signature: None,
400 tier: vyre_foundation::operation::OperationTier::Library,
401 laws: &[],
402 tolerance: vyre_foundation::operation::TolerancePolicy::EXACT,
403 id: OP_ID,
404 build: Some(|| gaussian_blur_2pass("input", "output", "scratch", 4, 4, 1, 0.8).horizontal),
405 test_inputs: Some(|| {
406 let pixels = vec![0xFFFF_FFFFu32; 16];
408 vec![vec![
409 crate::visual::byte_helpers::u32_words_to_le_bytes(&pixels), vec![0u8; 64], ]]
412 }),
413 expected_output: Some(|| {
414 let pixels = vec![0xFFFF_FFFFu32; 16];
416 vec![vec![crate::visual::byte_helpers::u32_words_to_le_bytes(&pixels)]]
417 }),
418 category: Some("visual"),
419 }
420}