1use bytemuck::{Pod, Zeroable};
36use wgpu::util::DeviceExt;
37
38const SIGMA_BLUR1: f32 = 2.0;
44const SIGMA_BLUR2: f32 = 3.0;
45const SIGMA_BLUR3: f32 = 4.0;
46
47#[repr(C)]
48#[derive(Debug, Clone, Copy, Pod, Zeroable)]
49struct BlurUniforms {
50 direction: [f32; 2],
51 texel_size: [f32; 2],
52 sigma: f32,
53 _pad0: f32,
54 _pad1: f32,
55 _pad2: f32,
56}
57
58pub struct BlurPipeline {
62 pipeline: wgpu::RenderPipeline,
63 bind_group_layout: wgpu::BindGroupLayout,
64 sampler: wgpu::Sampler,
65 uniform_buffers: [wgpu::Buffer; 6],
70 bind_groups: [wgpu::BindGroup; 6],
73}
74
75impl BlurPipeline {
76 #[allow(clippy::too_many_arguments)]
93 pub fn new(
94 device: &wgpu::Device,
95 target_format: wgpu::TextureFormat,
96 width: u32,
97 height: u32,
98 render_view: &wgpu::TextureView,
99 blur1_view: &wgpu::TextureView,
100 blur2_view: &wgpu::TextureView,
101 blur3_view: &wgpu::TextureView,
102 blur1_scratch_view: &wgpu::TextureView,
103 blur2_scratch_view: &wgpu::TextureView,
104 blur3_scratch_view: &wgpu::TextureView,
105 ) -> Self {
106 let _ = blur3_view;
110 let (w1, h1) = (width.max(2) / 2, height.max(2) / 2);
114 let (w2, h2) = (width.max(4) / 4, height.max(4) / 4);
115 let (w3, h3) = (width.max(8) / 8, height.max(8) / 8);
116 let texel_full = [1.0 / width as f32, 1.0 / height as f32];
117 let texel_b1 = [1.0 / w1 as f32, 1.0 / h1 as f32];
118 let texel_b2 = [1.0 / w2 as f32, 1.0 / h2 as f32];
119 let texel_b3 = [1.0 / w3 as f32, 1.0 / h3 as f32];
120
121 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
122 label: Some("Blur BGL"),
123 entries: &[
124 wgpu::BindGroupLayoutEntry {
125 binding: 0,
126 visibility: wgpu::ShaderStages::FRAGMENT,
127 ty: wgpu::BindingType::Buffer {
128 ty: wgpu::BufferBindingType::Uniform,
129 has_dynamic_offset: false,
130 min_binding_size: None,
131 },
132 count: None,
133 },
134 wgpu::BindGroupLayoutEntry {
135 binding: 1,
136 visibility: wgpu::ShaderStages::FRAGMENT,
137 ty: wgpu::BindingType::Texture {
138 sample_type: wgpu::TextureSampleType::Float { filterable: true },
139 view_dimension: wgpu::TextureViewDimension::D2,
140 multisampled: false,
141 },
142 count: None,
143 },
144 wgpu::BindGroupLayoutEntry {
145 binding: 2,
146 visibility: wgpu::ShaderStages::FRAGMENT,
147 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
148 count: None,
149 },
150 ],
151 });
152
153 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
154 label: Some("Blur Pipeline Layout"),
155 bind_group_layouts: &[Some(&bind_group_layout)],
156 immediate_size: 0,
157 });
158
159 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
160 label: Some("Blur Shader"),
161 source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/blur.wgsl").into()),
162 });
163
164 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
165 label: Some("Blur Pipeline"),
166 layout: Some(&pipeline_layout),
167 vertex: wgpu::VertexState {
168 module: &shader,
169 entry_point: Some("vs_main"),
170 buffers: &[],
171 compilation_options: Default::default(),
172 },
173 fragment: Some(wgpu::FragmentState {
174 module: &shader,
175 entry_point: Some("fs_main"),
176 targets: &[Some(wgpu::ColorTargetState {
177 format: target_format,
178 blend: Some(wgpu::BlendState::REPLACE),
179 write_mask: wgpu::ColorWrites::ALL,
180 })],
181 compilation_options: Default::default(),
182 }),
183 primitive: wgpu::PrimitiveState {
184 topology: wgpu::PrimitiveTopology::TriangleList,
185 ..Default::default()
186 },
187 depth_stencil: None,
188 multisample: wgpu::MultisampleState::default(),
189 multiview_mask: None,
190 cache: None,
191 });
192
193 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
194 label: Some("Blur Sampler"),
195 address_mode_u: wgpu::AddressMode::ClampToEdge,
196 address_mode_v: wgpu::AddressMode::ClampToEdge,
197 address_mode_w: wgpu::AddressMode::ClampToEdge,
198 mag_filter: wgpu::FilterMode::Linear,
199 min_filter: wgpu::FilterMode::Linear,
200 mipmap_filter: wgpu::MipmapFilterMode::Linear,
201 ..Default::default()
202 });
203
204 let make_uniforms = |dir: [f32; 2], texel: [f32; 2], sigma: f32| BlurUniforms {
209 direction: dir,
210 texel_size: texel,
211 sigma,
212 _pad0: 0.0,
213 _pad1: 0.0,
214 _pad2: 0.0,
215 };
216 let pass_specs: [BlurUniforms; 6] = [
217 make_uniforms([1.0, 0.0], texel_full, SIGMA_BLUR1),
219 make_uniforms([0.0, 1.0], texel_b1, SIGMA_BLUR1),
221 make_uniforms([1.0, 0.0], texel_b1, SIGMA_BLUR2),
223 make_uniforms([0.0, 1.0], texel_b2, SIGMA_BLUR2),
225 make_uniforms([1.0, 0.0], texel_b2, SIGMA_BLUR3),
227 make_uniforms([0.0, 1.0], texel_b3, SIGMA_BLUR3),
229 ];
230
231 let uniform_buffers: [wgpu::Buffer; 6] = std::array::from_fn(|i| {
232 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
233 label: Some(&format!("Blur Uniforms #{i}")),
234 contents: bytemuck::bytes_of(&pass_specs[i]),
235 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
236 })
237 });
238
239 let pass_sources: [&wgpu::TextureView; 6] = [
240 render_view, blur1_scratch_view, blur1_view, blur2_scratch_view, blur2_view, blur3_scratch_view, ];
247
248 let bind_groups: [wgpu::BindGroup; 6] = std::array::from_fn(|i| {
249 Self::make_bind_group(
250 device,
251 &bind_group_layout,
252 &uniform_buffers[i],
253 pass_sources[i],
254 &sampler,
255 i,
256 )
257 });
258
259 Self {
260 pipeline,
261 bind_group_layout,
262 sampler,
263 uniform_buffers,
264 bind_groups,
265 }
266 }
267
268 fn make_bind_group(
269 device: &wgpu::Device,
270 layout: &wgpu::BindGroupLayout,
271 uniforms: &wgpu::Buffer,
272 source: &wgpu::TextureView,
273 sampler: &wgpu::Sampler,
274 idx: usize,
275 ) -> wgpu::BindGroup {
276 device.create_bind_group(&wgpu::BindGroupDescriptor {
277 label: Some(&format!("Blur Bind Group #{idx}")),
278 layout,
279 entries: &[
280 wgpu::BindGroupEntry {
281 binding: 0,
282 resource: uniforms.as_entire_binding(),
283 },
284 wgpu::BindGroupEntry {
285 binding: 1,
286 resource: wgpu::BindingResource::TextureView(source),
287 },
288 wgpu::BindGroupEntry {
289 binding: 2,
290 resource: wgpu::BindingResource::Sampler(sampler),
291 },
292 ],
293 })
294 }
295
296 #[allow(clippy::too_many_arguments)]
299 pub fn rebind(
300 &mut self,
301 device: &wgpu::Device,
302 queue: &wgpu::Queue,
303 width: u32,
304 height: u32,
305 render_view: &wgpu::TextureView,
306 blur1_view: &wgpu::TextureView,
307 blur2_view: &wgpu::TextureView,
308 blur1_scratch_view: &wgpu::TextureView,
309 blur2_scratch_view: &wgpu::TextureView,
310 blur3_scratch_view: &wgpu::TextureView,
311 ) {
312 let (w1, h1) = (width.max(2) / 2, height.max(2) / 2);
313 let (w2, h2) = (width.max(4) / 4, height.max(4) / 4);
314 let (w3, h3) = (width.max(8) / 8, height.max(8) / 8);
315 let texel_full = [1.0 / width as f32, 1.0 / height as f32];
316 let texel_b1 = [1.0 / w1 as f32, 1.0 / h1 as f32];
317 let texel_b2 = [1.0 / w2 as f32, 1.0 / h2 as f32];
318 let texel_b3 = [1.0 / w3 as f32, 1.0 / h3 as f32];
319 let pass_uniforms: [(f32, [f32; 2], [f32; 2]); 6] = [
322 (SIGMA_BLUR1, texel_full, [1.0, 0.0]),
323 (SIGMA_BLUR1, texel_b1, [0.0, 1.0]),
324 (SIGMA_BLUR2, texel_b1, [1.0, 0.0]),
325 (SIGMA_BLUR2, texel_b2, [0.0, 1.0]),
326 (SIGMA_BLUR3, texel_b2, [1.0, 0.0]),
327 (SIGMA_BLUR3, texel_b3, [0.0, 1.0]),
328 ];
329 for (i, &(sigma, texel, direction)) in pass_uniforms.iter().enumerate() {
330 let u = BlurUniforms {
331 direction,
332 texel_size: texel,
333 sigma,
334 _pad0: 0.0,
335 _pad1: 0.0,
336 _pad2: 0.0,
337 };
338 queue.write_buffer(&self.uniform_buffers[i], 0, bytemuck::bytes_of(&u));
339 }
340
341 let pass_sources: [&wgpu::TextureView; 6] = [
342 render_view,
343 blur1_scratch_view,
344 blur1_view,
345 blur2_scratch_view,
346 blur2_view,
347 blur3_scratch_view,
348 ];
349 for (i, source) in pass_sources.iter().enumerate() {
350 self.bind_groups[i] = Self::make_bind_group(
351 device,
352 &self.bind_group_layout,
353 &self.uniform_buffers[i],
354 source,
355 &self.sampler,
356 i,
357 );
358 }
359 }
360
361 #[allow(clippy::too_many_arguments)]
365 pub fn render(
366 &self,
367 encoder: &mut wgpu::CommandEncoder,
368 blur1_view: &wgpu::TextureView,
369 blur2_view: &wgpu::TextureView,
370 blur3_view: &wgpu::TextureView,
371 blur1_scratch_view: &wgpu::TextureView,
372 blur2_scratch_view: &wgpu::TextureView,
373 blur3_scratch_view: &wgpu::TextureView,
374 ) {
375 let targets: [&wgpu::TextureView; 6] = [
376 blur1_scratch_view, blur1_view, blur2_scratch_view, blur2_view, blur3_scratch_view, blur3_view, ];
383 for (i, target) in targets.iter().enumerate() {
384 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
385 label: Some(&format!("Blur Pass #{i}")),
386 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
387 view: target,
388 depth_slice: None,
389 resolve_target: None,
390 ops: wgpu::Operations {
391 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
392 store: wgpu::StoreOp::Store,
393 },
394 })],
395 depth_stencil_attachment: None,
396 timestamp_writes: None,
397 occlusion_query_set: None,
398 multiview_mask: None,
399 });
400 pass.set_pipeline(&self.pipeline);
401 pass.set_bind_group(0, &self.bind_groups[i], &[]);
402 pass.draw(0..3, 0..1);
403 }
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410 use crate::chain_textures::ChainTextures;
411 use crate::config::{RenderConfig, TextureFormat};
412 use crate::gpu_context::GpuContext;
413
414 #[test]
415 fn blur_pipeline_instantiates() {
416 let cfg = RenderConfig::default();
417 let gpu = pollster::block_on(GpuContext::new(cfg)).unwrap();
418 let chain_tex = ChainTextures::new(&gpu.device, &gpu.config);
419 let _p = BlurPipeline::new(
420 &gpu.device,
421 gpu.config.texture_format.to_wgpu(),
422 gpu.config.width,
423 gpu.config.height,
424 &chain_tex.render_texture_view,
425 &chain_tex.blur1_texture_view,
426 &chain_tex.blur2_texture_view,
427 &chain_tex.blur3_texture_view,
428 &chain_tex.blur1_scratch_texture_view,
429 &chain_tex.blur2_scratch_texture_view,
430 &chain_tex.blur3_scratch_texture_view,
431 );
432 }
433
434 #[test]
441 fn blur_pipeline_spreads_energy_across_boundary() {
442 let cfg = RenderConfig {
443 width: 64,
444 height: 64,
445 texture_format: TextureFormat::Bgra8Unorm,
446 ..Default::default()
447 };
448 let gpu = pollster::block_on(GpuContext::new(cfg)).unwrap();
449 let chain_tex = ChainTextures::new(&gpu.device, &gpu.config);
450 let blur = BlurPipeline::new(
451 &gpu.device,
452 gpu.config.texture_format.to_wgpu(),
453 gpu.config.width,
454 gpu.config.height,
455 &chain_tex.render_texture_view,
456 &chain_tex.blur1_texture_view,
457 &chain_tex.blur2_texture_view,
458 &chain_tex.blur3_texture_view,
459 &chain_tex.blur1_scratch_texture_view,
460 &chain_tex.blur2_scratch_texture_view,
461 &chain_tex.blur3_scratch_texture_view,
462 );
463
464 let pixels: Vec<u8> = (0..64)
466 .flat_map(|_y| {
467 (0..64).flat_map(move |x| {
468 let v: u8 = if x < 32 { 0 } else { 255 };
469 [v, v, v, 255u8]
470 })
471 })
472 .collect();
473 gpu.queue.write_texture(
474 wgpu::TexelCopyTextureInfo {
475 texture: &chain_tex.render_texture,
476 mip_level: 0,
477 origin: wgpu::Origin3d::ZERO,
478 aspect: wgpu::TextureAspect::All,
479 },
480 &pixels,
481 wgpu::TexelCopyBufferLayout {
482 offset: 0,
483 bytes_per_row: Some(64 * 4),
484 rows_per_image: Some(64),
485 },
486 wgpu::Extent3d {
487 width: 64,
488 height: 64,
489 depth_or_array_layers: 1,
490 },
491 );
492
493 let mut encoder = gpu.device.create_command_encoder(&Default::default());
494 blur.render(
495 &mut encoder,
496 &chain_tex.blur1_texture_view,
497 &chain_tex.blur2_texture_view,
498 &chain_tex.blur3_texture_view,
499 &chain_tex.blur1_scratch_texture_view,
500 &chain_tex.blur2_scratch_texture_view,
501 &chain_tex.blur3_scratch_texture_view,
502 );
503 gpu.queue.submit(std::iter::once(encoder.finish()));
504
505 let blur_w: u32 = 32;
511 let blur_h: u32 = 32;
512 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
513 let unpadded_row: u32 = blur_w * 4;
514 let padded_row = unpadded_row.div_ceil(align) * align;
515 let staging = gpu.device.create_buffer(&wgpu::BufferDescriptor {
516 label: Some("Blur Test Staging"),
517 size: (padded_row * blur_h) as u64,
518 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
519 mapped_at_creation: false,
520 });
521 let mut e2 = gpu.device.create_command_encoder(&Default::default());
522 e2.copy_texture_to_buffer(
523 wgpu::TexelCopyTextureInfo {
524 texture: &chain_tex.blur1_texture,
525 mip_level: 0,
526 origin: wgpu::Origin3d::ZERO,
527 aspect: wgpu::TextureAspect::All,
528 },
529 wgpu::TexelCopyBufferInfo {
530 buffer: &staging,
531 layout: wgpu::TexelCopyBufferLayout {
532 offset: 0,
533 bytes_per_row: Some(padded_row),
534 rows_per_image: Some(blur_h),
535 },
536 },
537 wgpu::Extent3d {
538 width: blur_w,
539 height: blur_h,
540 depth_or_array_layers: 1,
541 },
542 );
543 gpu.queue.submit(std::iter::once(e2.finish()));
544
545 let (tx, rx) = std::sync::mpsc::channel();
546 staging.slice(..).map_async(wgpu::MapMode::Read, move |r| {
547 let _ = tx.send(r);
548 });
549 gpu.device.poll(wgpu::PollType::wait_indefinitely()).ok();
550 rx.recv().unwrap().unwrap();
551 let view = staging.slice(..).get_mapped_range();
552
553 let row = blur_h / 2;
556 let col = 15;
557 let off = (row * padded_row + col * 4) as usize;
558 let b = view[off];
559 assert!(
562 b > 20 && b < 200,
563 "boundary pixel should be partially blurred; got {b}"
564 );
565 drop(view);
566 staging.unmap();
567 }
568}