ailloli_ui_render_wgpu/
effect_chain.rs

1//! Post-effect chain for isolated offscreen passes (isolated compositor).
2
3use bytemuck::{Pod, Zeroable};
4use wgpu::util::DeviceExt;
5
6use crate::isolated_plan::{IsolatedEffect, IsolatedEffectChain};
7use crate::vertices::TexVertex;
8
9/// Uniform payload for one separable blur pass.
10#[repr(C)]
11#[derive(Clone, Copy, Pod, Zeroable)]
12struct BlurParamsGpu {
13    /// Unit sampling direction in texture-coordinate axes.
14    direction: [f32; 2],
15    /// Source texture width and height in physical texels.
16    tex_size: [f32; 2],
17    /// Blur radius in physical texels.
18    radius: f32,
19    /// Explicit alignment padding required by the GPU uniform layout.
20    _pad: f32,
21}
22
23/// Blur helper pipelines.
24///
25/// # Examples
26///
27/// ```
28/// use ailloli_ui_render_wgpu::effect_chain::EffectPipelines;
29/// let _: usize = std::mem::size_of::<EffectPipelines>();
30/// ```
31pub struct EffectPipelines {
32    /// Separable blur render pipeline.
33    pub blur: wgpu::RenderPipeline,
34    /// Uniform layout for direction, texture size, and radius.
35    pub blur_params_layout: wgpu::BindGroupLayout,
36    /// Source texture and sampler layout.
37    pub blur_tex_layout: wgpu::BindGroupLayout,
38    /// Linear sampler for blur taps.
39    pub sampler: wgpu::Sampler,
40    /// Six-vertex full-screen quad reused by both blur axes.
41    pub fullscreen_buf: wgpu::Buffer,
42}
43
44impl EffectPipelines {
45    /// Builds blur resources for one exact render-target format.
46    ///
47    /// # Examples
48    ///
49    /// ```no_run
50    /// use ailloli_ui_render_wgpu::effect_chain::EffectPipelines;
51    /// fn build(device: &wgpu::Device) -> EffectPipelines {
52    ///     EffectPipelines::new(device, wgpu::TextureFormat::Rgba8Unorm)
53    /// }
54    /// ```
55    pub fn new(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
56        let blur_params_layout =
57            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
58                label: Some("blur params layout"),
59                entries: &[wgpu::BindGroupLayoutEntry {
60                    binding: 0,
61                    visibility: wgpu::ShaderStages::FRAGMENT,
62                    ty: wgpu::BindingType::Buffer {
63                        ty: wgpu::BufferBindingType::Uniform,
64                        has_dynamic_offset: false,
65                        min_binding_size: None,
66                    },
67                    count: None,
68                }],
69            });
70        let blur_tex_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
71            label: Some("blur tex layout"),
72            entries: &[
73                wgpu::BindGroupLayoutEntry {
74                    binding: 0,
75                    visibility: wgpu::ShaderStages::FRAGMENT,
76                    ty: wgpu::BindingType::Texture {
77                        multisampled: false,
78                        view_dimension: wgpu::TextureViewDimension::D2,
79                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
80                    },
81                    count: None,
82                },
83                wgpu::BindGroupLayoutEntry {
84                    binding: 1,
85                    visibility: wgpu::ShaderStages::FRAGMENT,
86                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
87                    count: None,
88                },
89            ],
90        });
91        let blur_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
92            label: Some("blur layout"),
93            bind_group_layouts: &[&blur_params_layout, &blur_tex_layout],
94            push_constant_ranges: &[],
95        });
96        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
97            label: Some("blur shader"),
98            source: wgpu::ShaderSource::Wgsl(include_str!("shaders/blur.wgsl").into()),
99        });
100        let blur = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
101            label: Some("blur pipeline"),
102            layout: Some(&blur_layout),
103            vertex: wgpu::VertexState {
104                module: &shader,
105                entry_point: "vs_main",
106                compilation_options: Default::default(),
107                buffers: &[TexVertex::desc()],
108            },
109            fragment: Some(wgpu::FragmentState {
110                module: &shader,
111                entry_point: "fs_main",
112                compilation_options: Default::default(),
113                targets: &[Some(wgpu::ColorTargetState {
114                    format: surface_format,
115                    blend: None,
116                    write_mask: wgpu::ColorWrites::ALL,
117                })],
118            }),
119            primitive: wgpu::PrimitiveState::default(),
120            depth_stencil: None,
121            multisample: wgpu::MultisampleState::default(),
122            multiview: None,
123        });
124        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
125            label: Some("blur sampler"),
126            mag_filter: wgpu::FilterMode::Linear,
127            min_filter: wgpu::FilterMode::Linear,
128            ..Default::default()
129        });
130        let fullscreen = [
131            TexVertex {
132                pos: [-1.0, -1.0],
133                uv: [0.0, 1.0],
134                tint: [1.0; 4],
135            },
136            TexVertex {
137                pos: [1.0, -1.0],
138                uv: [1.0, 1.0],
139                tint: [1.0; 4],
140            },
141            TexVertex {
142                pos: [1.0, 1.0],
143                uv: [1.0, 0.0],
144                tint: [1.0; 4],
145            },
146            TexVertex {
147                pos: [-1.0, -1.0],
148                uv: [0.0, 1.0],
149                tint: [1.0; 4],
150            },
151            TexVertex {
152                pos: [1.0, 1.0],
153                uv: [1.0, 0.0],
154                tint: [1.0; 4],
155            },
156            TexVertex {
157                pos: [-1.0, 1.0],
158                uv: [0.0, 0.0],
159                tint: [1.0; 4],
160            },
161        ];
162        let fullscreen_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
163            label: Some("fullscreen tri"),
164            contents: bytemuck::cast_slice(&fullscreen),
165            usage: wgpu::BufferUsages::VERTEX,
166        });
167        Self {
168            blur,
169            blur_params_layout,
170            blur_tex_layout,
171            sampler,
172            fullscreen_buf,
173        }
174    }
175}
176
177/// Texture bindings produced by completed isolated passes.
178///
179/// # Examples
180///
181/// ```
182/// use ailloli_ui_render_wgpu::effect_chain::IsolatedCompositeTable;
183/// assert!(IsolatedCompositeTable::empty().bind_groups.is_empty());
184/// ```
185pub struct IsolatedCompositeTable {
186    /// Sample bindings indexed by frame-local pass ID.
187    pub bind_groups: std::collections::HashMap<u16, wgpu::BindGroup>,
188}
189
190impl IsolatedCompositeTable {
191    /// Creates an empty binding table.
192    ///
193    /// # Examples
194    ///
195    /// ```
196    /// use ailloli_ui_render_wgpu::effect_chain::IsolatedCompositeTable;
197    /// assert!(IsolatedCompositeTable::empty().get(0).is_none());
198    /// ```
199    pub fn empty() -> Self {
200        Self {
201            bind_groups: std::collections::HashMap::new(),
202        }
203    }
204
205    /// Returns the sample binding for a completed pass.
206    ///
207    /// # Examples
208    ///
209    /// ```
210    /// use ailloli_ui_render_wgpu::effect_chain::IsolatedCompositeTable;
211    /// assert!(IsolatedCompositeTable::empty().get(99).is_none());
212    /// ```
213    pub fn get(&self, id: u16) -> Option<&wgpu::BindGroup> {
214        self.bind_groups.get(&id)
215    }
216}
217
218/// Applies blur (if any) in-place on `src_view` using a transient ping-pong texture.
219///
220/// When several blur effects exist, only the largest strictly positive radius
221/// is used. Opacity effects are handled by compositing. A nonpositive or NaN
222/// maximum is a no-op. Dimensions are physical pixels; allocation clamps each
223/// axis to at least one, while uniforms preserve the supplied values.
224///
225/// # Examples
226///
227/// ```no_run
228/// use ailloli_ui_render_wgpu::{effect_chain::{run_effect_chain, EffectPipelines},
229///     IsolatedEffectChain};
230/// fn run(device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder,
231///     pipelines: &EffectPipelines, view: &wgpu::TextureView) {
232///     run_effect_chain(device, encoder, pipelines, wgpu::TextureFormat::Rgba8Unorm,
233///         view, 32, 32, &IsolatedEffectChain::default(), 0);
234/// }
235/// ```
236#[allow(clippy::too_many_arguments)]
237pub fn run_effect_chain(
238    device: &wgpu::Device,
239    encoder: &mut wgpu::CommandEncoder,
240    pipelines: &EffectPipelines,
241    format: wgpu::TextureFormat,
242    src_view: &wgpu::TextureView,
243    width: u32,
244    height: u32,
245    effects: &IsolatedEffectChain,
246    pass_id: u16,
247) {
248    let blur_radius = effects
249        .effects
250        .iter()
251        .filter_map(|e| match e {
252            IsolatedEffect::Blur { radius_px } if *radius_px > 0.0 => Some(*radius_px),
253            _ => None,
254        })
255        .fold(0.0f32, f32::max);
256
257    if blur_radius <= 0.0 {
258        return;
259    }
260
261    let ping_tex = device.create_texture(&wgpu::TextureDescriptor {
262        label: Some("blur ping"),
263        size: wgpu::Extent3d {
264            width: width.max(1),
265            height: height.max(1),
266            depth_or_array_layers: 1,
267        },
268        mip_level_count: 1,
269        sample_count: 1,
270        dimension: wgpu::TextureDimension::D2,
271        format,
272        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
273        view_formats: &[],
274    });
275    let ping_view = ping_tex.create_view(&wgpu::TextureViewDescriptor::default());
276
277    blur_pass(
278        device,
279        encoder,
280        pipelines,
281        src_view,
282        &ping_view,
283        width,
284        height,
285        [1.0, 0.0],
286        blur_radius,
287        pass_id,
288    );
289    blur_pass(
290        device,
291        encoder,
292        pipelines,
293        &ping_view,
294        src_view,
295        width,
296        height,
297        [0.0, 1.0],
298        blur_radius,
299        pass_id,
300    );
301}
302
303#[allow(clippy::too_many_arguments)]
304/// Encodes one horizontal or vertical blur pass into `encoder`.
305fn blur_pass(
306    device: &wgpu::Device,
307    encoder: &mut wgpu::CommandEncoder,
308    pipelines: &EffectPipelines,
309    src: &wgpu::TextureView,
310    dst: &wgpu::TextureView,
311    width: u32,
312    height: u32,
313    direction: [f32; 2],
314    radius: f32,
315    pass_id: u16,
316) {
317    let params = BlurParamsGpu {
318        direction,
319        tex_size: [width as f32, height as f32],
320        radius,
321        _pad: 0.0,
322    };
323    let params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
324        label: Some("blur params"),
325        contents: bytemuck::bytes_of(&params),
326        usage: wgpu::BufferUsages::UNIFORM,
327    });
328    let params_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
329        label: Some("blur params bg"),
330        layout: &pipelines.blur_params_layout,
331        entries: &[wgpu::BindGroupEntry {
332            binding: 0,
333            resource: params_buf.as_entire_binding(),
334        }],
335    });
336    let tex_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
337        label: Some("blur tex bg"),
338        layout: &pipelines.blur_tex_layout,
339        entries: &[
340            wgpu::BindGroupEntry {
341                binding: 0,
342                resource: wgpu::BindingResource::TextureView(src),
343            },
344            wgpu::BindGroupEntry {
345                binding: 1,
346                resource: wgpu::BindingResource::Sampler(&pipelines.sampler),
347            },
348        ],
349    });
350
351    let axis = if direction[0] > 0.5 { "H" } else { "V" };
352    let pass_label = format!("blur {axis} {pass_id}");
353    let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
354        label: Some(&pass_label),
355        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
356            view: dst,
357            resolve_target: None,
358            ops: wgpu::Operations {
359                load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
360                store: wgpu::StoreOp::Store,
361            },
362        })],
363        depth_stencil_attachment: None,
364        timestamp_writes: None,
365        occlusion_query_set: None,
366    });
367    pass.set_pipeline(&pipelines.blur);
368    pass.set_bind_group(0, &params_bg, &[]);
369    pass.set_bind_group(1, &tex_bg, &[]);
370    pass.set_vertex_buffer(0, pipelines.fullscreen_buf.slice(..));
371    pass.draw(0..6, 0..1);
372}