1#![forbid(unsafe_code)]
29
30use std::collections::HashMap;
31
32use backdrop_blur_core::{BackdropBlur, BlurError, BlurRequest, BlurStage, ResolvedMask};
33use wgpu::util::DeviceExt as _;
34
35mod cache;
36mod uniforms;
37
38use cache::{
39 PingPongKey, RETENTION_FRAMES, SCRATCH_FORMAT, TargetEncoding, backdrop_uv_remap,
40 composite_encode_srgb, evict_decision, kawase_halfpixel, kawase_level_size, resolve_gaussian,
41 resolve_kawase_levels, use_dual_kawase,
42};
43use uniforms::{CompositeParams, GaussianParams, KawaseParams};
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum SourceColorSpace {
52 GammaSrgb,
54 Linear,
56}
57
58pub struct SourceView {
62 pub view: wgpu::TextureView,
64 pub size: [u32; 2],
66 pub color_space: SourceColorSpace,
68}
69
70pub struct WgpuPrepared {
75 target_format: wgpu::TextureFormat,
76 generation: u64,
77 blur: PreparedBlur,
78 composite_bind: wgpu::BindGroup,
79}
80
81enum PreparedBlur {
85 Gaussian {
87 key: PingPongKey,
88 horizontal_bind: wgpu::BindGroup,
89 vertical_bind: wgpu::BindGroup,
90 },
91 DualKawase {
94 key: PingPongKey,
95 prefilter_bind: wgpu::BindGroup,
96 down_binds: Vec<wgpu::BindGroup>,
97 up_binds: Vec<wgpu::BindGroup>,
98 },
99}
100
101struct ScratchChain {
105 views: [wgpu::TextureView; 2],
106 last_used_frame: u64,
108}
109
110struct PyramidChain {
114 views: Vec<wgpu::TextureView>,
115 last_used_frame: u64,
117}
118
119pub struct WgpuBlur {
124 pipeline_layout: wgpu::PipelineLayout,
125 bind_group_layout: wgpu::BindGroupLayout,
126 sampler: wgpu::Sampler,
127 gaussian_pipeline: wgpu::RenderPipeline,
128 downsample_pipeline: wgpu::RenderPipeline,
129 upsample_pipeline: wgpu::RenderPipeline,
130 composite_shader: wgpu::ShaderModule,
131 composite_pipelines: HashMap<wgpu::TextureFormat, wgpu::RenderPipeline>,
132 scratch: HashMap<PingPongKey, ScratchChain>,
133 pyramids: HashMap<PingPongKey, PyramidChain>,
135 frame: u64,
139 generation: u64,
141}
142
143impl WgpuBlur {
146 pub fn new(device: &wgpu::Device) -> Self {
149 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
150 label: Some("backdrop-blur bind group layout"),
151 entries: &[
152 wgpu::BindGroupLayoutEntry {
153 binding: 0,
154 visibility: wgpu::ShaderStages::FRAGMENT,
155 ty: wgpu::BindingType::Texture {
156 sample_type: wgpu::TextureSampleType::Float { filterable: true },
157 view_dimension: wgpu::TextureViewDimension::D2,
158 multisampled: false,
159 },
160 count: None,
161 },
162 wgpu::BindGroupLayoutEntry {
163 binding: 1,
164 visibility: wgpu::ShaderStages::FRAGMENT,
165 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
166 count: None,
167 },
168 wgpu::BindGroupLayoutEntry {
169 binding: 2,
170 visibility: wgpu::ShaderStages::FRAGMENT,
171 ty: wgpu::BindingType::Buffer {
172 ty: wgpu::BufferBindingType::Uniform,
173 has_dynamic_offset: false,
174 min_binding_size: None,
175 },
176 count: None,
177 },
178 ],
179 });
180
181 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
182 label: Some("backdrop-blur pipeline layout"),
183 bind_group_layouts: &[Some(&bind_group_layout)],
184 immediate_size: 0,
185 });
186
187 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
188 label: Some("backdrop-blur sampler"),
189 address_mode_u: wgpu::AddressMode::ClampToEdge,
190 address_mode_v: wgpu::AddressMode::ClampToEdge,
191 address_mode_w: wgpu::AddressMode::ClampToEdge,
192 mag_filter: wgpu::FilterMode::Linear,
193 min_filter: wgpu::FilterMode::Linear,
194 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
195 ..Default::default()
196 });
197
198 let gaussian_shader =
199 device.create_shader_module(wgpu::include_wgsl!("shaders/gaussian.wgsl"));
200 let downsample_shader =
201 device.create_shader_module(wgpu::include_wgsl!("shaders/downsample.wgsl"));
202 let upsample_shader =
203 device.create_shader_module(wgpu::include_wgsl!("shaders/upsample.wgsl"));
204 let composite_shader =
205 device.create_shader_module(wgpu::include_wgsl!("shaders/composite.wgsl"));
206
207 let gaussian_pipeline = build_pipeline(
210 device,
211 &pipeline_layout,
212 &gaussian_shader,
213 SCRATCH_FORMAT,
214 None,
215 );
216 let downsample_pipeline = build_pipeline(
217 device,
218 &pipeline_layout,
219 &downsample_shader,
220 SCRATCH_FORMAT,
221 None,
222 );
223 let upsample_pipeline = build_pipeline(
224 device,
225 &pipeline_layout,
226 &upsample_shader,
227 SCRATCH_FORMAT,
228 None,
229 );
230
231 Self {
232 pipeline_layout,
233 bind_group_layout,
234 sampler,
235 gaussian_pipeline,
236 downsample_pipeline,
237 upsample_pipeline,
238 composite_shader,
239 composite_pipelines: HashMap::new(),
240 scratch: HashMap::new(),
241 pyramids: HashMap::new(),
242 frame: 0,
243 generation: 0,
244 }
245 }
246}
247
248impl WgpuBlur {
251 fn begin_frame(&mut self) {
257 self.frame = self.frame.wrapping_add(1);
258 let stale = evict_decision(
259 self.scratch.iter().map(|(k, c)| (*k, c.last_used_frame)),
260 self.frame,
261 RETENTION_FRAMES,
262 );
263 for key in stale {
264 self.scratch.remove(&key);
265 }
266 let stale = evict_decision(
267 self.pyramids.iter().map(|(k, c)| (*k, c.last_used_frame)),
268 self.frame,
269 RETENTION_FRAMES,
270 );
271 for key in stale {
272 self.pyramids.remove(&key);
273 }
274 }
275
276 fn ensure_scratch(&mut self, device: &wgpu::Device, key: PingPongKey) {
279 if let Some(chain) = self.scratch.get_mut(&key) {
280 chain.last_used_frame = self.frame;
281 return;
282 }
283 let view_a = scratch_view(device, key.size, "backdrop-blur scratch A");
284 let view_b = scratch_view(device, key.size, "backdrop-blur scratch B");
285 self.scratch.insert(
286 key,
287 ScratchChain {
288 views: [view_a, view_b],
289 last_used_frame: self.frame,
290 },
291 );
292 }
293
294 fn ensure_pyramid(&mut self, device: &wgpu::Device, key: PingPongKey) {
297 if let Some(chain) = self.pyramids.get_mut(&key) {
298 chain.last_used_frame = self.frame;
299 return;
300 }
301 let views = (0..=key.levels)
302 .map(|level| {
303 let size = kawase_level_size(key.size, level);
304 scratch_view(device, size, "backdrop-blur kawase mip")
305 })
306 .collect();
307 self.pyramids.insert(
308 key,
309 PyramidChain {
310 views,
311 last_used_frame: self.frame,
312 },
313 );
314 }
315
316 fn ensure_composite_pipeline(&mut self, device: &wgpu::Device, format: wgpu::TextureFormat) {
318 if self.composite_pipelines.contains_key(&format) {
319 return;
320 }
321 let pipeline = build_pipeline(
322 device,
323 &self.pipeline_layout,
324 &self.composite_shader,
325 format,
326 Some(over_blend()),
327 );
328 self.composite_pipelines.insert(format, pipeline);
329 }
330
331 fn bind(
333 &self,
334 device: &wgpu::Device,
335 view: &wgpu::TextureView,
336 uniform: &wgpu::Buffer,
337 label: &str,
338 ) -> wgpu::BindGroup {
339 device.create_bind_group(&wgpu::BindGroupDescriptor {
340 label: Some(label),
341 layout: &self.bind_group_layout,
342 entries: &[
343 wgpu::BindGroupEntry {
344 binding: 0,
345 resource: wgpu::BindingResource::TextureView(view),
346 },
347 wgpu::BindGroupEntry {
348 binding: 1,
349 resource: wgpu::BindingResource::Sampler(&self.sampler),
350 },
351 wgpu::BindGroupEntry {
352 binding: 2,
353 resource: uniform.as_entire_binding(),
354 },
355 ],
356 })
357 }
358}
359
360#[cfg(feature = "image-snapshots")]
363impl WgpuBlur {
364 pub fn cached_chain_count(&self) -> usize {
368 self.scratch.len() + self.pyramids.len()
369 }
370}
371
372impl BackdropBlur for WgpuBlur {
375 type Device = wgpu::Device;
376 type Queue = wgpu::Queue;
377 type Encoder = wgpu::CommandEncoder;
378 type SourceTexture = SourceView;
379 type Target = wgpu::TextureView;
380 type TargetFormat = wgpu::TextureFormat;
381 type Prepared = WgpuPrepared;
382
383 fn prepare(
384 &mut self,
385 device: &Self::Device,
386 _queue: &Self::Queue,
387 source: &Self::SourceTexture,
388 target_format: Self::TargetFormat,
389 request: &BlurRequest,
390 ) -> Result<Option<Self::Prepared>, BlurError> {
391 let Some(clipped) = request.source_region.clip_to(source.size) else {
392 return Ok(None); };
394
395 self.begin_frame();
401
402 let encode_srgb = matches!(
403 composite_encode_srgb(target_format).ok_or_else(|| BlurError::UnsupportedTarget {
404 format: format!("{target_format:?}"),
405 })?,
406 TargetEncoding::Srgb
407 );
408 let decode_srgb = matches!(source.color_space, SourceColorSpace::GammaSrgb);
409 self.ensure_composite_pipeline(device, target_format);
410
411 let radius = request.physical_blur_radius();
412 let [source_w, source_h] = [source.size[0] as f32, source.size[1] as f32];
413 let [clip_x, clip_y] = [clipped.origin[0] as f32, clipped.origin[1] as f32];
414 let [clip_w, clip_h] = [clipped.size[0] as f32, clipped.size[1] as f32];
415 let remap_offset = [clip_x / source_w, clip_y / source_h];
418 let remap_scale = [clip_w / source_w, clip_h / source_h];
419
420 let (backdrop_uv_offset, backdrop_uv_scale) =
424 backdrop_uv_remap(&request.source_region, &clipped);
425 let mask = ResolvedMask::from_target(&request.target_rect, request.corner_radius);
426 let tint = request.tint.color();
427 let composite = CompositeParams::new(
428 [
429 request.target_rect.origin[0] as f32,
430 request.target_rect.origin[1] as f32,
431 ],
432 [
433 request.target_rect.size[0] as f32,
434 request.target_rect.size[1] as f32,
435 ],
436 [tint.r(), tint.g(), tint.b(), tint.a()],
437 backdrop_uv_offset,
438 backdrop_uv_scale,
439 mask.corner_radius_px,
440 encode_srgb,
441 request.opacity.value(),
442 );
443 let composite_buf = uniform_buffer(device, &composite, "backdrop-blur composite");
444
445 let (blur, composite_bind) = if use_dual_kawase(radius) {
446 let levels = resolve_kawase_levels(radius);
447 let key = PingPongKey {
448 size: clipped.size,
449 levels,
450 };
451 self.ensure_pyramid(device, key);
452 let n = levels as usize;
453
454 let prefilter = GaussianParams::new(
457 remap_offset,
458 remap_scale,
459 [1.0 / source_w, 1.0 / source_h],
460 [1.0, 0.0],
461 0.5,
462 0,
463 decode_srgb,
464 );
465 let prefilter_buf =
466 uniform_buffer(device, &prefilter, "backdrop-blur kawase-prefilter");
467 let down_bufs: Vec<wgpu::Buffer> = (0..n)
469 .map(|i| {
470 let hp = kawase_halfpixel(kawase_level_size(clipped.size, i as u32));
471 uniform_buffer(device, &KawaseParams::new(hp), "backdrop-blur kawase-down")
472 })
473 .collect();
474 let up_bufs: Vec<wgpu::Buffer> = (0..n)
475 .map(|j| {
476 let hp = kawase_halfpixel(kawase_level_size(clipped.size, (n - j) as u32));
477 uniform_buffer(device, &KawaseParams::new(hp), "backdrop-blur kawase-up")
478 })
479 .collect();
480
481 let pyramid = self
482 .pyramids
483 .get(&key)
484 .ok_or_else(|| BlurError::ResourceCreation {
485 stage: BlurStage::PingPongTexture,
486 source: "kawase pyramid missing immediately after ensure_pyramid".into(),
487 })?;
488 let pyramid = &pyramid.views;
489 let prefilter_bind = self.bind(
490 device,
491 &source.view,
492 &prefilter_buf,
493 "backdrop-blur prefilter-bind",
494 );
495 let down_binds = down_bufs
496 .iter()
497 .enumerate()
498 .map(|(i, buf)| self.bind(device, &pyramid[i], buf, "backdrop-blur down-bind"))
499 .collect();
500 let up_binds = up_bufs
501 .iter()
502 .enumerate()
503 .map(|(j, buf)| self.bind(device, &pyramid[n - j], buf, "backdrop-blur up-bind"))
504 .collect();
505 let composite_bind = self.bind(
506 device,
507 &pyramid[0],
508 &composite_buf,
509 "backdrop-blur composite-bind",
510 );
511
512 (
513 PreparedBlur::DualKawase {
514 key,
515 prefilter_bind,
516 down_binds,
517 up_binds,
518 },
519 composite_bind,
520 )
521 } else {
522 let kernel = resolve_gaussian(radius);
523 let key = PingPongKey {
524 size: clipped.size,
525 levels: 1,
526 };
527 self.ensure_scratch(device, key);
528
529 let horizontal = GaussianParams::new(
532 remap_offset,
533 remap_scale,
534 [1.0 / source_w, 1.0 / source_h],
535 [1.0, 0.0],
536 kernel.sigma,
537 kernel.tap_radius,
538 decode_srgb,
539 );
540 let vertical = GaussianParams::new(
541 [0.0, 0.0],
542 [1.0, 1.0],
543 [1.0 / clip_w, 1.0 / clip_h],
544 [0.0, 1.0],
545 kernel.sigma,
546 kernel.tap_radius,
547 false,
548 );
549 let horizontal_buf = uniform_buffer(device, &horizontal, "backdrop-blur gaussian-h");
550 let vertical_buf = uniform_buffer(device, &vertical, "backdrop-blur gaussian-v");
551
552 let chain = self
553 .scratch
554 .get(&key)
555 .ok_or_else(|| BlurError::ResourceCreation {
556 stage: BlurStage::PingPongTexture,
557 source: "scratch chain missing immediately after ensure_scratch".into(),
558 })?;
559 let horizontal_bind = self.bind(
560 device,
561 &source.view,
562 &horizontal_buf,
563 "backdrop-blur h-bind",
564 );
565 let vertical_bind = self.bind(
566 device,
567 &chain.views[0],
568 &vertical_buf,
569 "backdrop-blur v-bind",
570 );
571 let composite_bind = self.bind(
572 device,
573 &chain.views[1],
574 &composite_buf,
575 "backdrop-blur composite-bind",
576 );
577
578 (
579 PreparedBlur::Gaussian {
580 key,
581 horizontal_bind,
582 vertical_bind,
583 },
584 composite_bind,
585 )
586 };
587
588 self.generation += 1;
589 Ok(Some(WgpuPrepared {
590 target_format,
591 generation: self.generation,
592 blur,
593 composite_bind,
594 }))
595 }
596
597 fn record(
598 &self,
599 encoder: &mut Self::Encoder,
600 target: &Self::Target,
601 prepared: &Self::Prepared,
602 ) -> Result<(), BlurError> {
603 debug_assert_eq!(
607 prepared.generation, self.generation,
608 "record called with a stale Prepared (a newer prepare clobbered the shared scratch); \
609 v1 requires serial prepare→record per surface (K1)"
610 );
611 let composite_pipeline = self
612 .composite_pipelines
613 .get(&prepared.target_format)
614 .ok_or_else(|| BlurError::ResourceCreation {
615 stage: BlurStage::CompositePipeline,
616 source: "composite pipeline missing at record".into(),
617 })?;
618
619 self.record_blur(encoder, &prepared.blur)?;
621
622 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
629 label: Some("backdrop-blur composite-pass"),
630 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
631 view: target,
632 resolve_target: None,
633 depth_slice: None,
634 ops: wgpu::Operations {
635 load: wgpu::LoadOp::Load,
636 store: wgpu::StoreOp::Store,
637 },
638 })],
639 depth_stencil_attachment: None,
640 timestamp_writes: None,
641 occlusion_query_set: None,
642 multiview_mask: None,
643 });
644 pass.set_pipeline(composite_pipeline);
645 pass.set_bind_group(0, &prepared.composite_bind, &[]);
646 pass.draw(0..3, 0..1);
647 Ok(())
648 }
649}
650
651fn uniform_buffer<T: bytemuck::Pod>(device: &wgpu::Device, value: &T, label: &str) -> wgpu::Buffer {
655 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
656 label: Some(label),
657 contents: bytemuck::bytes_of(value),
658 usage: wgpu::BufferUsages::UNIFORM,
659 })
660}
661
662fn scratch_view(device: &wgpu::Device, size: [u32; 2], label: &str) -> wgpu::TextureView {
665 let texture = device.create_texture(&wgpu::TextureDescriptor {
666 label: Some(label),
667 size: wgpu::Extent3d {
668 width: size[0],
669 height: size[1],
670 depth_or_array_layers: 1,
671 },
672 mip_level_count: 1,
673 sample_count: 1,
674 dimension: wgpu::TextureDimension::D2,
675 format: SCRATCH_FORMAT,
676 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
677 view_formats: &[],
678 });
679 texture.create_view(&wgpu::TextureViewDescriptor::default())
680}
681
682impl WgpuBlur {
683 fn record_blur(
686 &self,
687 encoder: &mut wgpu::CommandEncoder,
688 blur: &PreparedBlur,
689 ) -> Result<(), BlurError> {
690 let missing = || BlurError::ResourceCreation {
691 stage: BlurStage::PingPongTexture,
692 source: "scratch missing at record (prepare not called, or evicted)".into(),
693 };
694 match blur {
695 PreparedBlur::Gaussian {
696 key,
697 horizontal_bind,
698 vertical_bind,
699 } => {
700 let chain = self.scratch.get(key).ok_or_else(missing)?;
701 self.blur_pass(
702 encoder,
703 &chain.views[0],
704 horizontal_bind,
705 &self.gaussian_pipeline,
706 "backdrop-blur h-pass",
707 );
708 self.blur_pass(
709 encoder,
710 &chain.views[1],
711 vertical_bind,
712 &self.gaussian_pipeline,
713 "backdrop-blur v-pass",
714 );
715 }
716 PreparedBlur::DualKawase {
717 key,
718 prefilter_bind,
719 down_binds,
720 up_binds,
721 } => {
722 let pyramid = self.pyramids.get(key).ok_or_else(missing)?;
723 let pyramid = &pyramid.views;
724 let n = key.levels as usize;
725 self.blur_pass(
728 encoder,
729 &pyramid[0],
730 prefilter_bind,
731 &self.gaussian_pipeline,
732 "backdrop-blur kawase-prefilter",
733 );
734 for (i, bind) in down_binds.iter().enumerate() {
736 self.blur_pass(
737 encoder,
738 &pyramid[i + 1],
739 bind,
740 &self.downsample_pipeline,
741 "backdrop-blur kawase-down",
742 );
743 }
744 for (j, bind) in up_binds.iter().enumerate() {
746 self.blur_pass(
747 encoder,
748 &pyramid[n - 1 - j],
749 bind,
750 &self.upsample_pipeline,
751 "backdrop-blur kawase-up",
752 );
753 }
754 }
755 }
756 Ok(())
757 }
758
759 fn blur_pass(
762 &self,
763 encoder: &mut wgpu::CommandEncoder,
764 attachment: &wgpu::TextureView,
765 bind: &wgpu::BindGroup,
766 pipeline: &wgpu::RenderPipeline,
767 label: &str,
768 ) {
769 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
770 label: Some(label),
771 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
772 view: attachment,
773 resolve_target: None,
774 depth_slice: None,
775 ops: wgpu::Operations {
776 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
777 store: wgpu::StoreOp::Store,
778 },
779 })],
780 depth_stencil_attachment: None,
781 timestamp_writes: None,
782 occlusion_query_set: None,
783 multiview_mask: None,
784 });
785 pass.set_pipeline(pipeline);
786 pass.set_bind_group(0, bind, &[]);
787 pass.draw(0..3, 0..1);
788 }
789}
790
791fn over_blend() -> wgpu::BlendState {
793 wgpu::BlendState {
794 color: wgpu::BlendComponent {
795 src_factor: wgpu::BlendFactor::SrcAlpha,
796 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
797 operation: wgpu::BlendOperation::Add,
798 },
799 alpha: wgpu::BlendComponent {
800 src_factor: wgpu::BlendFactor::One,
801 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
802 operation: wgpu::BlendOperation::Add,
803 },
804 }
805}
806
807fn build_pipeline(
810 device: &wgpu::Device,
811 layout: &wgpu::PipelineLayout,
812 shader: &wgpu::ShaderModule,
813 format: wgpu::TextureFormat,
814 blend: Option<wgpu::BlendState>,
815) -> wgpu::RenderPipeline {
816 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
817 label: Some("backdrop-blur pipeline"),
818 layout: Some(layout),
819 vertex: wgpu::VertexState {
820 module: shader,
821 entry_point: Some("vs_main"),
822 buffers: &[],
823 compilation_options: wgpu::PipelineCompilationOptions::default(),
824 },
825 fragment: Some(wgpu::FragmentState {
826 module: shader,
827 entry_point: Some("fs_main"),
828 targets: &[Some(wgpu::ColorTargetState {
829 format,
830 blend,
831 write_mask: wgpu::ColorWrites::ALL,
832 })],
833 compilation_options: wgpu::PipelineCompilationOptions::default(),
834 }),
835 primitive: wgpu::PrimitiveState {
836 topology: wgpu::PrimitiveTopology::TriangleList,
837 ..Default::default()
838 },
839 depth_stencil: None,
840 multisample: wgpu::MultisampleState::default(),
841 multiview_mask: None,
842 cache: None,
843 })
844}