Skip to main content

zenthra_render/
blit_pipeline.rs

1/// blit_pipeline.rs
2/// Fullscreen blit: copies one 2D texture to the current render target.
3/// Used for:
4///   1. Presenting the offscreen render target to the surface at end of frame
5///   2. (Optional) compositing blurred regions in the backdrop pass
6
7#[repr(C)]
8#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
9pub struct BackdropUniforms {
10    pub radius: [f32; 4],
11    pub rect_pos: [f32; 2],
12    pub rect_size: [f32; 2],
13    pub screen_size: [f32; 2],
14    pub time: f32,
15    pub brightness: f32,
16    pub saturation: f32,
17    pub contrast: f32,
18    pub blur_type: f32,
19    pub opacity: f32,
20    pub padding: [f32; 2],
21    pub _end_padding: [f32; 2],
22}
23
24pub struct BlitPipeline {
25    pub pipeline:          wgpu::RenderPipeline,
26    pub backdrop_pipeline: wgpu::RenderPipeline,
27    pub bgl:               wgpu::BindGroupLayout,
28    pub backdrop_bgl:      wgpu::BindGroupLayout,
29    pub sampler:           wgpu::Sampler,
30}
31
32impl BlitPipeline {
33    pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
34        let shader = device.create_shader_module(wgpu::include_wgsl!("shaders/blit.wgsl"));
35
36        let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
37            label: Some("Blit BGL"),
38            entries: &[
39                wgpu::BindGroupLayoutEntry {
40                    binding: 0,
41                    visibility: wgpu::ShaderStages::FRAGMENT,
42                    ty: wgpu::BindingType::Texture {
43                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
44                        view_dimension: wgpu::TextureViewDimension::D2,
45                        multisampled: false,
46                    },
47                    count: None,
48                },
49                wgpu::BindGroupLayoutEntry {
50                    binding: 1,
51                    visibility: wgpu::ShaderStages::FRAGMENT,
52                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
53                    count: None,
54                },
55            ],
56        });
57
58        let backdrop_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
59            label: Some("Blit Backdrop BGL"),
60            entries: &[
61                wgpu::BindGroupLayoutEntry {
62                    binding: 0,
63                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
64                    ty: wgpu::BindingType::Buffer {
65                        ty: wgpu::BufferBindingType::Uniform,
66                        has_dynamic_offset: false,
67                        min_binding_size: None,
68                    },
69                    count: None,
70                },
71            ],
72        });
73
74        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
75            label: Some("Blit Pipeline Layout"),
76            bind_group_layouts: &[Some(&bgl)],
77            immediate_size: 0,
78        });
79
80        let backdrop_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
81            label: Some("Blit Backdrop Pipeline Layout"),
82            bind_group_layouts: &[Some(&bgl), Some(&backdrop_bgl)],
83            immediate_size: 0,
84        });
85
86        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
87            label: Some("Blit Pipeline"),
88            layout: Some(&layout),
89            vertex: wgpu::VertexState {
90                module: &shader,
91                entry_point: Some("vs_main"),
92                buffers: &[],
93                compilation_options: Default::default(),
94            },
95            fragment: Some(wgpu::FragmentState {
96                module: &shader,
97                entry_point: Some("fs_main"),
98                targets: &[Some(wgpu::ColorTargetState {
99                    format,
100                    blend: None, // fully replace — used for present blit
101                    write_mask: wgpu::ColorWrites::ALL,
102                })],
103                compilation_options: Default::default(),
104            }),
105            primitive: wgpu::PrimitiveState::default(),
106            depth_stencil: None,
107            multisample: wgpu::MultisampleState::default(),
108            multiview_mask: None,
109            cache: None,
110        });
111
112        let backdrop_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
113            label: Some("Blit Backdrop Pipeline"),
114            layout: Some(&backdrop_layout),
115            vertex: wgpu::VertexState {
116                module: &shader,
117                entry_point: Some("vs_backdrop"),
118                buffers: &[],
119                compilation_options: Default::default(),
120            },
121            fragment: Some(wgpu::FragmentState {
122                module: &shader,
123                entry_point: Some("fs_backdrop"),
124                targets: &[Some(wgpu::ColorTargetState {
125                    format,
126                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
127                    write_mask: wgpu::ColorWrites::ALL,
128                })],
129                compilation_options: Default::default(),
130            }),
131            primitive: wgpu::PrimitiveState::default(),
132            depth_stencil: None,
133            multisample: wgpu::MultisampleState::default(),
134            multiview_mask: None,
135            cache: None,
136        });
137
138        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
139            label: Some("Blit Sampler"),
140            address_mode_u: wgpu::AddressMode::ClampToEdge,
141            address_mode_v: wgpu::AddressMode::ClampToEdge,
142            mag_filter: wgpu::FilterMode::Linear,
143            min_filter: wgpu::FilterMode::Linear,
144            ..Default::default()
145        });
146
147        Self { pipeline, backdrop_pipeline, bgl, backdrop_bgl, sampler }
148    }
149
150    /// Blit `src_view` into the current render pass target.
151    /// Caller must have already begun the render pass.
152    pub fn blit<'a>(
153        &'a self,
154        device: &wgpu::Device,
155        pass: &mut wgpu::RenderPass<'a>,
156        src_view: &wgpu::TextureView,
157    ) {
158        let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
159            label: Some("Blit BG"),
160            layout: &self.bgl,
161            entries: &[
162                wgpu::BindGroupEntry {
163                    binding: 0,
164                    resource: wgpu::BindingResource::TextureView(src_view),
165                },
166                wgpu::BindGroupEntry {
167                    binding: 1,
168                    resource: wgpu::BindingResource::Sampler(&self.sampler),
169                },
170            ],
171        });
172        pass.set_pipeline(&self.pipeline);
173        pass.set_bind_group(0, &bg, &[]);
174        pass.draw(0..3, 0..1);
175    }
176
177    /// Convenience: open a render pass, blit, and close it.
178    pub fn blit_to(
179        &self,
180        device: &wgpu::Device,
181        encoder: &mut wgpu::CommandEncoder,
182        src_view: &wgpu::TextureView,
183        dst_view: &wgpu::TextureView,
184        load: wgpu::LoadOp<wgpu::Color>,
185    ) {
186        let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
187            label: Some("Blit BG"),
188            layout: &self.bgl,
189            entries: &[
190                wgpu::BindGroupEntry {
191                    binding: 0,
192                    resource: wgpu::BindingResource::TextureView(src_view),
193                },
194                wgpu::BindGroupEntry {
195                    binding: 1,
196                    resource: wgpu::BindingResource::Sampler(&self.sampler),
197                },
198            ],
199        });
200
201        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
202            label: Some("Blit Pass"),
203            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
204                view: dst_view,
205                resolve_target: None,
206                ops: wgpu::Operations { load, store: wgpu::StoreOp::Store },
207                depth_slice: None,
208            })],
209            depth_stencil_attachment: None,
210            timestamp_writes: None,
211            occlusion_query_set: None,
212            multiview_mask: None,
213        });
214
215        pass.set_pipeline(&self.pipeline);
216        pass.set_bind_group(0, &bg, &[]);
217        pass.draw(0..3, 0..1);
218    }
219
220    /// Blit the blurred texture into `dst_view`, clipped to a specific rounded rect region.
221    pub fn blit_to_clipped(
222        &self,
223        device: &wgpu::Device,
224        encoder: &mut wgpu::CommandEncoder,
225        src_view: &wgpu::TextureView,
226        dst_view: &wgpu::TextureView,
227        rect_pos: [f32; 2],
228        rect_size: [f32; 2],
229        radius: [f32; 4],
230        surface_w: u32,
231        surface_h: u32,
232        brightness: f32,
233        saturation: f32,
234        contrast: f32,
235        blur_type: f32,
236        opacity: f32,
237    ) {
238        use wgpu::util::DeviceExt;
239
240        let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
241            label: Some("Blit Clipped BG"),
242            layout: &self.bgl,
243            entries: &[
244                wgpu::BindGroupEntry {
245                    binding: 0,
246                    resource: wgpu::BindingResource::TextureView(src_view),
247                },
248                wgpu::BindGroupEntry {
249                    binding: 1,
250                    resource: wgpu::BindingResource::Sampler(&self.sampler),
251                },
252            ],
253        });
254
255        let uniforms = BackdropUniforms {
256            radius,
257            rect_pos,
258            rect_size,
259            screen_size: [surface_w as f32, surface_h as f32],
260            time: 0.0,
261            brightness,
262            saturation,
263            contrast,
264            blur_type,
265            opacity,
266            padding: [0.0; 2],
267            _end_padding: [0.0; 2],
268        };
269
270        let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
271            label: Some("Blit Backdrop Uniform Buffer"),
272            contents: bytemuck::bytes_of(&uniforms),
273            usage: wgpu::BufferUsages::UNIFORM,
274        });
275
276        let uniform_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
277            label: Some("Blit Backdrop Uniform BG"),
278            layout: &self.backdrop_bgl,
279            entries: &[
280                wgpu::BindGroupEntry {
281                    binding: 0,
282                    resource: uniform_buf.as_entire_binding(),
283                },
284            ],
285        });
286
287        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
288            label: Some("Blit Clipped Pass"),
289            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
290                view: dst_view,
291                resolve_target: None,
292                // Load existing content, blend rounded blurred rect on top
293                ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store },
294                depth_slice: None,
295            })],
296            depth_stencil_attachment: None,
297            timestamp_writes: None,
298            occlusion_query_set: None,
299            multiview_mask: None,
300        });
301
302        pass.set_pipeline(&self.backdrop_pipeline);
303        pass.set_bind_group(0, &bg, &[]);
304        pass.set_bind_group(1, &uniform_bg, &[]);
305        pass.draw(0..6, 0..1);
306    }
307}