Skip to main content

wisp/filter/
blur.rs

1//! `BlurFilter` — two-pass separable Gaussian blur.
2
3use bytemuck::{Pod, Zeroable};
4use wgpu::util::DeviceExt;
5
6use crate::application::Application;
7use crate::filter::{Filter, FilterContext};
8use crate::texture::render_texture::RenderTexture;
9
10/// Separable Gaussian blur, 9-tap horizontal + 9-tap vertical.
11///
12/// `radius` is the texel-space spread (`1.0` ≈ subtle, `8.0` heavy). `passes`
13/// returns 2 — the orchestrator runs us once with `pass=0` (horizontal) and
14/// once with `pass=1` (vertical) using ping-pong render targets.
15#[derive(Debug, Clone, Copy)]
16pub struct BlurFilter {
17    /// Texel-space spread per pass.
18    pub radius: f32,
19}
20
21impl BlurFilter {
22    /// Construct a blur with an explicit radius.
23    #[must_use]
24    pub const fn new(radius: f32) -> Self {
25        Self { radius }
26    }
27}
28
29impl Default for BlurFilter {
30    fn default() -> Self {
31        Self { radius: 4.0 }
32    }
33}
34
35#[repr(C)]
36#[derive(Clone, Copy, Pod, Zeroable)]
37struct BlurUniforms {
38    direction: [f32; 2],
39    radius: f32,
40    _pad: f32,
41}
42
43impl Filter for BlurFilter {
44    fn passes(&self) -> u32 {
45        2
46    }
47
48    fn render_pass(
49        &self,
50        ctx: &mut FilterContext<'_>,
51        input: &RenderTexture,
52        output: &RenderTexture,
53        pass: u32,
54    ) {
55        let direction = if pass == 0 {
56            [1.0_f32, 0.0]
57        } else {
58            [0.0, 1.0]
59        };
60        run_blur_pass(ctx.app, ctx.encoder, input, output, direction, self.radius);
61    }
62}
63
64pub(crate) fn run_blur_pass(
65    app: &Application,
66    encoder: &mut wgpu::CommandEncoder,
67    input: &RenderTexture,
68    output: &RenderTexture,
69    direction: [f32; 2],
70    radius: f32,
71) {
72    let device = app.device();
73    let pipeline_cache = filter_pipeline(app, output.format());
74
75    let uniforms = BlurUniforms {
76        direction,
77        radius,
78        _pad: 0.0,
79    };
80    let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
81        label: Some("wisp::blur uniforms"),
82        contents: bytemuck::bytes_of(&uniforms),
83        usage: wgpu::BufferUsages::UNIFORM,
84    });
85
86    let uniform_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
87        label: Some("wisp::blur uniform bg"),
88        layout: &pipeline_cache.uniform_layout,
89        entries: &[wgpu::BindGroupEntry {
90            binding: 0,
91            resource: uniform_buffer.as_entire_binding(),
92        }],
93    });
94
95    let texture_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
96        label: Some("wisp::blur texture bg"),
97        layout: &pipeline_cache.texture_layout,
98        entries: &[
99            wgpu::BindGroupEntry {
100                binding: 0,
101                resource: wgpu::BindingResource::TextureView(input.view()),
102            },
103            wgpu::BindGroupEntry {
104                binding: 1,
105                resource: wgpu::BindingResource::Sampler(input.sampler()),
106            },
107        ],
108    });
109
110    let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
111        label: Some("wisp::blur pass"),
112        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
113            view: output.view(),
114            resolve_target: None,
115            ops: wgpu::Operations {
116                load: wgpu::LoadOp::Load,
117                store: wgpu::StoreOp::Store,
118            },
119        })],
120        depth_stencil_attachment: None,
121        timestamp_writes: None,
122        occlusion_query_set: None,
123    });
124    pass.set_pipeline(&pipeline_cache.pipeline);
125    pass.set_bind_group(0, &uniform_bg, &[]);
126    pass.set_bind_group(1, &texture_bg, &[]);
127    pass.draw(0..3, 0..1);
128}
129
130/// Pipeline cache. M0.16 builds a fresh one each call; M1+ can promote it
131/// to a `Renderer` field if perf demands.
132struct BlurPipelineCache {
133    pipeline: wgpu::RenderPipeline,
134    uniform_layout: wgpu::BindGroupLayout,
135    texture_layout: wgpu::BindGroupLayout,
136}
137
138fn filter_pipeline(app: &Application, format: wgpu::TextureFormat) -> BlurPipelineCache {
139    let device = app.device();
140    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
141        label: Some("wisp::filter_blur"),
142        source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/filter_blur.wgsl").into()),
143    });
144
145    let uniform_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
146        label: Some("wisp::blur uniforms layout"),
147        entries: &[wgpu::BindGroupLayoutEntry {
148            binding: 0,
149            visibility: wgpu::ShaderStages::FRAGMENT,
150            ty: wgpu::BindingType::Buffer {
151                ty: wgpu::BufferBindingType::Uniform,
152                has_dynamic_offset: false,
153                min_binding_size: None,
154            },
155            count: None,
156        }],
157    });
158    let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
159        label: Some("wisp::blur texture layout"),
160        entries: &[
161            wgpu::BindGroupLayoutEntry {
162                binding: 0,
163                visibility: wgpu::ShaderStages::FRAGMENT,
164                ty: wgpu::BindingType::Texture {
165                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
166                    view_dimension: wgpu::TextureViewDimension::D2,
167                    multisampled: false,
168                },
169                count: None,
170            },
171            wgpu::BindGroupLayoutEntry {
172                binding: 1,
173                visibility: wgpu::ShaderStages::FRAGMENT,
174                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
175                count: None,
176            },
177        ],
178    });
179
180    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
181        label: Some("wisp::blur pipeline layout"),
182        bind_group_layouts: &[&uniform_layout, &texture_layout],
183        push_constant_ranges: &[],
184    });
185
186    let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
187        label: Some("wisp::blur pipeline"),
188        layout: Some(&pipeline_layout),
189        vertex: wgpu::VertexState {
190            module: &shader,
191            entry_point: Some("main_vs"),
192            buffers: &[],
193            compilation_options: wgpu::PipelineCompilationOptions::default(),
194        },
195        fragment: Some(wgpu::FragmentState {
196            module: &shader,
197            entry_point: Some("main_fs"),
198            targets: &[Some(wgpu::ColorTargetState {
199                format,
200                blend: None,
201                write_mask: wgpu::ColorWrites::ALL,
202            })],
203            compilation_options: wgpu::PipelineCompilationOptions::default(),
204        }),
205        primitive: wgpu::PrimitiveState::default(),
206        depth_stencil: None,
207        multisample: wgpu::MultisampleState::default(),
208        multiview: None,
209        cache: None,
210    });
211
212    BlurPipelineCache {
213        pipeline,
214        uniform_layout,
215        texture_layout,
216    }
217}