#![forbid(unsafe_code)]
use std::collections::HashMap;
use backdrop_blur_core::{BackdropBlur, BlurError, BlurRequest, BlurStage, ResolvedMask};
use wgpu::util::DeviceExt as _;
mod cache;
mod uniforms;
use cache::{
PingPongKey, RETENTION_FRAMES, SCRATCH_FORMAT, TargetEncoding, backdrop_uv_remap,
composite_encode_srgb, evict_decision, kawase_halfpixel, kawase_level_size, resolve_gaussian,
resolve_kawase_levels, use_dual_kawase,
};
use uniforms::{CompositeParams, GaussianParams, KawaseParams};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SourceColorSpace {
GammaSrgb,
Linear,
}
pub struct SourceView {
pub view: wgpu::TextureView,
pub size: [u32; 2],
pub color_space: SourceColorSpace,
}
pub struct WgpuPrepared {
target_format: wgpu::TextureFormat,
generation: u64,
blur: PreparedBlur,
composite_bind: wgpu::BindGroup,
}
enum PreparedBlur {
Gaussian {
key: PingPongKey,
horizontal_bind: wgpu::BindGroup,
vertical_bind: wgpu::BindGroup,
},
DualKawase {
key: PingPongKey,
prefilter_bind: wgpu::BindGroup,
down_binds: Vec<wgpu::BindGroup>,
up_binds: Vec<wgpu::BindGroup>,
},
}
struct ScratchChain {
views: [wgpu::TextureView; 2],
last_used_frame: u64,
}
struct PyramidChain {
views: Vec<wgpu::TextureView>,
last_used_frame: u64,
}
pub struct WgpuBlur {
pipeline_layout: wgpu::PipelineLayout,
bind_group_layout: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
gaussian_pipeline: wgpu::RenderPipeline,
downsample_pipeline: wgpu::RenderPipeline,
upsample_pipeline: wgpu::RenderPipeline,
composite_shader: wgpu::ShaderModule,
composite_pipelines: HashMap<wgpu::TextureFormat, wgpu::RenderPipeline>,
scratch: HashMap<PingPongKey, ScratchChain>,
pyramids: HashMap<PingPongKey, PyramidChain>,
frame: u64,
generation: u64,
}
impl WgpuBlur {
pub fn new(device: &wgpu::Device) -> Self {
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("backdrop-blur bind group layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("backdrop-blur pipeline layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("backdrop-blur sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
let gaussian_shader =
device.create_shader_module(wgpu::include_wgsl!("shaders/gaussian.wgsl"));
let downsample_shader =
device.create_shader_module(wgpu::include_wgsl!("shaders/downsample.wgsl"));
let upsample_shader =
device.create_shader_module(wgpu::include_wgsl!("shaders/upsample.wgsl"));
let composite_shader =
device.create_shader_module(wgpu::include_wgsl!("shaders/composite.wgsl"));
let gaussian_pipeline = build_pipeline(
device,
&pipeline_layout,
&gaussian_shader,
SCRATCH_FORMAT,
None,
);
let downsample_pipeline = build_pipeline(
device,
&pipeline_layout,
&downsample_shader,
SCRATCH_FORMAT,
None,
);
let upsample_pipeline = build_pipeline(
device,
&pipeline_layout,
&upsample_shader,
SCRATCH_FORMAT,
None,
);
Self {
pipeline_layout,
bind_group_layout,
sampler,
gaussian_pipeline,
downsample_pipeline,
upsample_pipeline,
composite_shader,
composite_pipelines: HashMap::new(),
scratch: HashMap::new(),
pyramids: HashMap::new(),
frame: 0,
generation: 0,
}
}
}
impl WgpuBlur {
fn begin_frame(&mut self) {
self.frame = self.frame.wrapping_add(1);
let stale = evict_decision(
self.scratch.iter().map(|(k, c)| (*k, c.last_used_frame)),
self.frame,
RETENTION_FRAMES,
);
for key in stale {
self.scratch.remove(&key);
}
let stale = evict_decision(
self.pyramids.iter().map(|(k, c)| (*k, c.last_used_frame)),
self.frame,
RETENTION_FRAMES,
);
for key in stale {
self.pyramids.remove(&key);
}
}
fn ensure_scratch(&mut self, device: &wgpu::Device, key: PingPongKey) {
if let Some(chain) = self.scratch.get_mut(&key) {
chain.last_used_frame = self.frame;
return;
}
let view_a = scratch_view(device, key.size, "backdrop-blur scratch A");
let view_b = scratch_view(device, key.size, "backdrop-blur scratch B");
self.scratch.insert(
key,
ScratchChain {
views: [view_a, view_b],
last_used_frame: self.frame,
},
);
}
fn ensure_pyramid(&mut self, device: &wgpu::Device, key: PingPongKey) {
if let Some(chain) = self.pyramids.get_mut(&key) {
chain.last_used_frame = self.frame;
return;
}
let views = (0..=key.levels)
.map(|level| {
let size = kawase_level_size(key.size, level);
scratch_view(device, size, "backdrop-blur kawase mip")
})
.collect();
self.pyramids.insert(
key,
PyramidChain {
views,
last_used_frame: self.frame,
},
);
}
fn ensure_composite_pipeline(&mut self, device: &wgpu::Device, format: wgpu::TextureFormat) {
if self.composite_pipelines.contains_key(&format) {
return;
}
let pipeline = build_pipeline(
device,
&self.pipeline_layout,
&self.composite_shader,
format,
Some(over_blend()),
);
self.composite_pipelines.insert(format, pipeline);
}
fn bind(
&self,
device: &wgpu::Device,
view: &wgpu::TextureView,
uniform: &wgpu::Buffer,
label: &str,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(label),
layout: &self.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: uniform.as_entire_binding(),
},
],
})
}
}
#[cfg(feature = "image-snapshots")]
impl WgpuBlur {
pub fn cached_chain_count(&self) -> usize {
self.scratch.len() + self.pyramids.len()
}
}
impl BackdropBlur for WgpuBlur {
type Device = wgpu::Device;
type Queue = wgpu::Queue;
type Encoder = wgpu::CommandEncoder;
type SourceTexture = SourceView;
type Target = wgpu::TextureView;
type TargetFormat = wgpu::TextureFormat;
type Prepared = WgpuPrepared;
fn prepare(
&mut self,
device: &Self::Device,
_queue: &Self::Queue,
source: &Self::SourceTexture,
target_format: Self::TargetFormat,
request: &BlurRequest,
) -> Result<Option<Self::Prepared>, BlurError> {
let Some(clipped) = request.source_region.clip_to(source.size) else {
return Ok(None); };
self.begin_frame();
let encode_srgb = matches!(
composite_encode_srgb(target_format).ok_or_else(|| BlurError::UnsupportedTarget {
format: format!("{target_format:?}"),
})?,
TargetEncoding::Srgb
);
let decode_srgb = matches!(source.color_space, SourceColorSpace::GammaSrgb);
self.ensure_composite_pipeline(device, target_format);
let radius = request.physical_blur_radius();
let [source_w, source_h] = [source.size[0] as f32, source.size[1] as f32];
let [clip_x, clip_y] = [clipped.origin[0] as f32, clipped.origin[1] as f32];
let [clip_w, clip_h] = [clipped.size[0] as f32, clipped.size[1] as f32];
let remap_offset = [clip_x / source_w, clip_y / source_h];
let remap_scale = [clip_w / source_w, clip_h / source_h];
let (backdrop_uv_offset, backdrop_uv_scale) =
backdrop_uv_remap(&request.source_region, &clipped);
let mask = ResolvedMask::from_target(&request.target_rect, request.corner_radius);
let tint = request.tint.color();
let composite = CompositeParams::new(
[
request.target_rect.origin[0] as f32,
request.target_rect.origin[1] as f32,
],
[
request.target_rect.size[0] as f32,
request.target_rect.size[1] as f32,
],
[tint.r(), tint.g(), tint.b(), tint.a()],
backdrop_uv_offset,
backdrop_uv_scale,
mask.corner_radius_px,
encode_srgb,
request.opacity.value(),
);
let composite_buf = uniform_buffer(device, &composite, "backdrop-blur composite");
let (blur, composite_bind) = if use_dual_kawase(radius) {
let levels = resolve_kawase_levels(radius);
let key = PingPongKey {
size: clipped.size,
levels,
};
self.ensure_pyramid(device, key);
let n = levels as usize;
let prefilter = GaussianParams::new(
remap_offset,
remap_scale,
[1.0 / source_w, 1.0 / source_h],
[1.0, 0.0],
0.5,
0,
decode_srgb,
);
let prefilter_buf =
uniform_buffer(device, &prefilter, "backdrop-blur kawase-prefilter");
let down_bufs: Vec<wgpu::Buffer> = (0..n)
.map(|i| {
let hp = kawase_halfpixel(kawase_level_size(clipped.size, i as u32));
uniform_buffer(device, &KawaseParams::new(hp), "backdrop-blur kawase-down")
})
.collect();
let up_bufs: Vec<wgpu::Buffer> = (0..n)
.map(|j| {
let hp = kawase_halfpixel(kawase_level_size(clipped.size, (n - j) as u32));
uniform_buffer(device, &KawaseParams::new(hp), "backdrop-blur kawase-up")
})
.collect();
let pyramid = self
.pyramids
.get(&key)
.ok_or_else(|| BlurError::ResourceCreation {
stage: BlurStage::PingPongTexture,
source: "kawase pyramid missing immediately after ensure_pyramid".into(),
})?;
let pyramid = &pyramid.views;
let prefilter_bind = self.bind(
device,
&source.view,
&prefilter_buf,
"backdrop-blur prefilter-bind",
);
let down_binds = down_bufs
.iter()
.enumerate()
.map(|(i, buf)| self.bind(device, &pyramid[i], buf, "backdrop-blur down-bind"))
.collect();
let up_binds = up_bufs
.iter()
.enumerate()
.map(|(j, buf)| self.bind(device, &pyramid[n - j], buf, "backdrop-blur up-bind"))
.collect();
let composite_bind = self.bind(
device,
&pyramid[0],
&composite_buf,
"backdrop-blur composite-bind",
);
(
PreparedBlur::DualKawase {
key,
prefilter_bind,
down_binds,
up_binds,
},
composite_bind,
)
} else {
let kernel = resolve_gaussian(radius);
let key = PingPongKey {
size: clipped.size,
levels: 1,
};
self.ensure_scratch(device, key);
let horizontal = GaussianParams::new(
remap_offset,
remap_scale,
[1.0 / source_w, 1.0 / source_h],
[1.0, 0.0],
kernel.sigma,
kernel.tap_radius,
decode_srgb,
);
let vertical = GaussianParams::new(
[0.0, 0.0],
[1.0, 1.0],
[1.0 / clip_w, 1.0 / clip_h],
[0.0, 1.0],
kernel.sigma,
kernel.tap_radius,
false,
);
let horizontal_buf = uniform_buffer(device, &horizontal, "backdrop-blur gaussian-h");
let vertical_buf = uniform_buffer(device, &vertical, "backdrop-blur gaussian-v");
let chain = self
.scratch
.get(&key)
.ok_or_else(|| BlurError::ResourceCreation {
stage: BlurStage::PingPongTexture,
source: "scratch chain missing immediately after ensure_scratch".into(),
})?;
let horizontal_bind = self.bind(
device,
&source.view,
&horizontal_buf,
"backdrop-blur h-bind",
);
let vertical_bind = self.bind(
device,
&chain.views[0],
&vertical_buf,
"backdrop-blur v-bind",
);
let composite_bind = self.bind(
device,
&chain.views[1],
&composite_buf,
"backdrop-blur composite-bind",
);
(
PreparedBlur::Gaussian {
key,
horizontal_bind,
vertical_bind,
},
composite_bind,
)
};
self.generation += 1;
Ok(Some(WgpuPrepared {
target_format,
generation: self.generation,
blur,
composite_bind,
}))
}
fn record(
&self,
encoder: &mut Self::Encoder,
target: &Self::Target,
prepared: &Self::Prepared,
) -> Result<(), BlurError> {
debug_assert_eq!(
prepared.generation, self.generation,
"record called with a stale Prepared (a newer prepare clobbered the shared scratch); \
v1 requires serial prepare→record per surface (K1)"
);
let composite_pipeline = self
.composite_pipelines
.get(&prepared.target_format)
.ok_or_else(|| BlurError::ResourceCreation {
stage: BlurStage::CompositePipeline,
source: "composite pipeline missing at record".into(),
})?;
self.record_blur(encoder, &prepared.blur)?;
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("backdrop-blur composite-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(composite_pipeline);
pass.set_bind_group(0, &prepared.composite_bind, &[]);
pass.draw(0..3, 0..1);
Ok(())
}
}
fn uniform_buffer<T: bytemuck::Pod>(device: &wgpu::Device, value: &T, label: &str) -> wgpu::Buffer {
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: bytemuck::bytes_of(value),
usage: wgpu::BufferUsages::UNIFORM,
})
}
fn scratch_view(device: &wgpu::Device, size: [u32; 2], label: &str) -> wgpu::TextureView {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width: size[0],
height: size[1],
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: SCRATCH_FORMAT,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
texture.create_view(&wgpu::TextureViewDescriptor::default())
}
impl WgpuBlur {
fn record_blur(
&self,
encoder: &mut wgpu::CommandEncoder,
blur: &PreparedBlur,
) -> Result<(), BlurError> {
let missing = || BlurError::ResourceCreation {
stage: BlurStage::PingPongTexture,
source: "scratch missing at record (prepare not called, or evicted)".into(),
};
match blur {
PreparedBlur::Gaussian {
key,
horizontal_bind,
vertical_bind,
} => {
let chain = self.scratch.get(key).ok_or_else(missing)?;
self.blur_pass(
encoder,
&chain.views[0],
horizontal_bind,
&self.gaussian_pipeline,
"backdrop-blur h-pass",
);
self.blur_pass(
encoder,
&chain.views[1],
vertical_bind,
&self.gaussian_pipeline,
"backdrop-blur v-pass",
);
}
PreparedBlur::DualKawase {
key,
prefilter_bind,
down_binds,
up_binds,
} => {
let pyramid = self.pyramids.get(key).ok_or_else(missing)?;
let pyramid = &pyramid.views;
let n = key.levels as usize;
self.blur_pass(
encoder,
&pyramid[0],
prefilter_bind,
&self.gaussian_pipeline,
"backdrop-blur kawase-prefilter",
);
for (i, bind) in down_binds.iter().enumerate() {
self.blur_pass(
encoder,
&pyramid[i + 1],
bind,
&self.downsample_pipeline,
"backdrop-blur kawase-down",
);
}
for (j, bind) in up_binds.iter().enumerate() {
self.blur_pass(
encoder,
&pyramid[n - 1 - j],
bind,
&self.upsample_pipeline,
"backdrop-blur kawase-up",
);
}
}
}
Ok(())
}
fn blur_pass(
&self,
encoder: &mut wgpu::CommandEncoder,
attachment: &wgpu::TextureView,
bind: &wgpu::BindGroup,
pipeline: &wgpu::RenderPipeline,
label: &str,
) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(label),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: attachment,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(pipeline);
pass.set_bind_group(0, bind, &[]);
pass.draw(0..3, 0..1);
}
}
fn over_blend() -> wgpu::BlendState {
wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
}
}
fn build_pipeline(
device: &wgpu::Device,
layout: &wgpu::PipelineLayout,
shader: &wgpu::ShaderModule,
format: wgpu::TextureFormat,
blend: Option<wgpu::BlendState>,
) -> wgpu::RenderPipeline {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("backdrop-blur pipeline"),
layout: Some(layout),
vertex: wgpu::VertexState {
module: shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
})
}