ailloli_ui_render_wgpu/
composite_blend.rs

1//! blend modes: shader composite (Multiply / Screen) with dst framebuffer capture.
2
3use ailloli_ui_runtime::BlendMode;
4use bytemuck::{Pod, Zeroable};
5use wgpu::util::DeviceExt;
6
7use crate::isolated_plan::PlannedIsolatedComposite;
8use crate::vertices::TexVertex;
9
10/// Uniform payload selecting blend equation and source opacity.
11#[repr(C)]
12#[derive(Clone, Copy, Pod, Zeroable)]
13struct CompositeBlendParamsGpu {
14    /// Shader-side numeric identifier for the requested blend mode.
15    mode: u32,
16    /// Finite source opacity applied before blending.
17    opacity: f32,
18    /// Explicit alignment padding required by the GPU uniform layout.
19    _pad: [f32; 2],
20}
21
22/// Maps the provider-neutral blend enum to its stable shader identifier.
23fn blend_mode_shader_id(mode: BlendMode) -> u32 {
24    match mode {
25        BlendMode::Multiply => 1,
26        BlendMode::Screen => 2,
27        BlendMode::Normal => 0,
28    }
29}
30
31/// Pipelines for compositing an isolated fg texture over a captured dst region.
32///
33/// # Examples
34///
35/// ```
36/// use ailloli_ui_render_wgpu::composite_blend::CompositeBlendPipelines;
37/// let _: usize = std::mem::size_of::<CompositeBlendPipelines>();
38/// ```
39pub struct CompositeBlendPipelines {
40    /// Render pipeline implementing normal, multiply, and screen equations.
41    pub pipeline: wgpu::RenderPipeline,
42    /// Uniform layout for shader mode and clamped opacity.
43    pub params_layout: wgpu::BindGroupLayout,
44    /// Shared texture-plus-sampler layout for foreground and background.
45    pub tex_layout: wgpu::BindGroupLayout,
46    /// Linear sampler used for both captured textures.
47    pub sampler: wgpu::Sampler,
48}
49
50impl CompositeBlendPipelines {
51    /// Builds blend resources for one exact output format.
52    ///
53    /// # Examples
54    ///
55    /// ```no_run
56    /// use ailloli_ui_render_wgpu::composite_blend::CompositeBlendPipelines;
57    /// fn build(device: &wgpu::Device) -> CompositeBlendPipelines {
58    ///     CompositeBlendPipelines::new(device, wgpu::TextureFormat::Bgra8UnormSrgb)
59    /// }
60    /// ```
61    pub fn new(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
62        let params_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
63            label: Some("composite blend params layout"),
64            entries: &[wgpu::BindGroupLayoutEntry {
65                binding: 0,
66                visibility: wgpu::ShaderStages::FRAGMENT,
67                ty: wgpu::BindingType::Buffer {
68                    ty: wgpu::BufferBindingType::Uniform,
69                    has_dynamic_offset: false,
70                    min_binding_size: None,
71                },
72                count: None,
73            }],
74        });
75        let tex_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
76            label: Some("composite blend tex layout"),
77            entries: &[
78                wgpu::BindGroupLayoutEntry {
79                    binding: 0,
80                    visibility: wgpu::ShaderStages::FRAGMENT,
81                    ty: wgpu::BindingType::Texture {
82                        multisampled: false,
83                        view_dimension: wgpu::TextureViewDimension::D2,
84                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
85                    },
86                    count: None,
87                },
88                wgpu::BindGroupLayoutEntry {
89                    binding: 1,
90                    visibility: wgpu::ShaderStages::FRAGMENT,
91                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
92                    count: None,
93                },
94            ],
95        });
96        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
97            label: Some("composite blend layout"),
98            bind_group_layouts: &[&params_layout, &tex_layout, &tex_layout],
99            push_constant_ranges: &[],
100        });
101        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
102            label: Some("composite blend shader"),
103            source: wgpu::ShaderSource::Wgsl(include_str!("shaders/composite_blend.wgsl").into()),
104        });
105        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
106            label: Some("composite blend pipeline"),
107            layout: Some(&layout),
108            vertex: wgpu::VertexState {
109                module: &shader,
110                entry_point: "vs_main",
111                compilation_options: Default::default(),
112                buffers: &[TexVertex::desc()],
113            },
114            fragment: Some(wgpu::FragmentState {
115                module: &shader,
116                entry_point: "fs_main",
117                compilation_options: Default::default(),
118                targets: &[Some(wgpu::ColorTargetState {
119                    format: surface_format,
120                    blend: None,
121                    write_mask: wgpu::ColorWrites::ALL,
122                })],
123            }),
124            primitive: wgpu::PrimitiveState::default(),
125            depth_stencil: None,
126            multisample: wgpu::MultisampleState::default(),
127            multiview: None,
128        });
129        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
130            label: Some("composite blend sampler"),
131            mag_filter: wgpu::FilterMode::Linear,
132            min_filter: wgpu::FilterMode::Linear,
133            ..Default::default()
134        });
135        Self {
136            pipeline,
137            params_layout,
138            tex_layout,
139            sampler,
140        }
141    }
142}
143
144/// Draw fg over captured bg into the main pass (`view` must use `Load`).
145///
146/// Opacity is clamped to `[0, 1]`. The function creates transient uniform and
147/// background bind groups, preserves existing main-frame color, and draws the
148/// composite's half-open vertex range. All views and the pipeline must share a
149/// compatible format and device.
150///
151/// # Examples
152///
153/// ```no_run
154/// use ailloli_ui_render_wgpu::{composite_blend::{draw_composite_blend,
155///     CompositeBlendPipelines}, isolated_plan::PlannedIsolatedComposite};
156/// fn draw(device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder,
157///     pipelines: &CompositeBlendPipelines, fg: &wgpu::BindGroup,
158///     bg: &wgpu::TextureView, comp: &PlannedIsolatedComposite,
159///     vertices: &wgpu::Buffer, main: &wgpu::TextureView) {
160///     draw_composite_blend(device, encoder, pipelines, fg, bg, comp, vertices, main, None);
161/// }
162/// ```
163#[allow(clippy::too_many_arguments)]
164pub fn draw_composite_blend(
165    device: &wgpu::Device,
166    encoder: &mut wgpu::CommandEncoder,
167    pipelines: &CompositeBlendPipelines,
168    fg_bind_group: &wgpu::BindGroup,
169    bg_view: &wgpu::TextureView,
170    comp: &PlannedIsolatedComposite,
171    composite_buf: &wgpu::Buffer,
172    main_view: &wgpu::TextureView,
173    depth_stencil: Option<&wgpu::RenderPassDepthStencilAttachment<'_>>,
174) {
175    let params = CompositeBlendParamsGpu {
176        mode: blend_mode_shader_id(comp.blend_mode),
177        opacity: comp.opacity.clamp(0.0, 1.0),
178        _pad: [0.0; 2],
179    };
180    let params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
181        label: Some("composite blend params"),
182        contents: bytemuck::bytes_of(&params),
183        usage: wgpu::BufferUsages::UNIFORM,
184    });
185    let params_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
186        label: Some("composite blend params bg"),
187        layout: &pipelines.params_layout,
188        entries: &[wgpu::BindGroupEntry {
189            binding: 0,
190            resource: params_buf.as_entire_binding(),
191        }],
192    });
193    let bg_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
194        label: Some("composite blend bg tex"),
195        layout: &pipelines.tex_layout,
196        entries: &[
197            wgpu::BindGroupEntry {
198                binding: 0,
199                resource: wgpu::BindingResource::TextureView(bg_view),
200            },
201            wgpu::BindGroupEntry {
202                binding: 1,
203                resource: wgpu::BindingResource::Sampler(&pipelines.sampler),
204            },
205        ],
206    });
207
208    let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
209        label: Some("composite blend to main"),
210        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
211            view: main_view,
212            resolve_target: None,
213            ops: wgpu::Operations {
214                load: wgpu::LoadOp::Load,
215                store: wgpu::StoreOp::Store,
216            },
217        })],
218        depth_stencil_attachment: depth_stencil.cloned(),
219        timestamp_writes: None,
220        occlusion_query_set: None,
221    });
222    pass.set_pipeline(&pipelines.pipeline);
223    pass.set_bind_group(0, &params_bg, &[]);
224    pass.set_bind_group(1, fg_bind_group, &[]);
225    pass.set_bind_group(2, &bg_bg, &[]);
226    pass.set_vertex_buffer(0, composite_buf.slice(..));
227    pass.draw(comp.vertex_range.clone(), 0..1);
228}