use bytemuck::Zeroable;
use wgpu::util::DeviceExt;
use crate::shape::PlacedShape;
use crate::transform::Transform;
use crate::widgets::{Color, Rect};
use super::gpu::{EMPTY_CLIP_RECT, NO_CLIP_RECT};
const WORKING_SCALE: u32 = 4;
const IDLE_FRAMES_BEFORE_RELEASE: u32 = 120;
const MAX_TAPS: f32 = 24.0;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct Params {
src_rect: [f32; 4],
direction: [f32; 2],
sigma: f32,
taps: f32,
dst_size: [f32; 2],
curvature: f32,
_pad1: f32,
radii: [f32; 4],
shape_rect: [f32; 4],
viewport_origin: [f32; 2],
mask_size: [f32; 2],
to_shape: [f32; 6],
_pad3: [f32; 2],
stroke_color: [f32; 4],
stroke_width: f32,
_pad4: [f32; 3],
clip_rect: [f32; 4],
clip_radii: [f32; 4],
to_clip: [f32; 6],
clip_curvature: f32,
_pad6: f32,
}
#[derive(Clone, Copy)]
pub struct CoverageMask<'a> {
pub view: &'a wgpu::TextureView,
pub size: (u32, u32),
}
#[derive(Debug, Clone, Copy)]
pub struct BackdropRegion {
pub shape: PlacedShape,
pub scale: f32,
pub radius: f32,
pub clip: Option<PlacedShape>,
}
impl BackdropRegion {
pub(crate) fn viewport(&self) -> Rect {
let world = self.shape.world_aabb();
Rect::new(
world.x * self.scale,
world.y * self.scale,
world.width * self.scale,
world.height * self.scale,
)
}
fn clamped(&self, width: u32, height: u32) -> Option<(u32, u32, u32, u32)> {
let rect = self.viewport();
let mut left = rect.x;
let mut top = rect.y;
let mut right_f = rect.x + rect.width;
let mut bottom_f = rect.y + rect.height;
if let Some(clip) = self.clip {
let clip = clip.world_aabb();
left = left.max(clip.x * self.scale);
top = top.max(clip.y * self.scale);
right_f = right_f.min((clip.x + clip.width) * self.scale);
bottom_f = bottom_f.min((clip.y + clip.height) * self.scale);
}
let x = left.floor().max(0.0) as u32;
let y = top.floor().max(0.0) as u32;
let right = right_f.ceil().max(0.0) as u32;
let bottom = bottom_f.ceil().max(0.0) as u32;
let right = right.min(width);
let bottom = bottom.min(height);
if x >= right || y >= bottom {
return None;
}
Some((x, y, right - x, bottom - y))
}
fn shape_params(&self, origin: (u32, u32), mask: Option<CoverageMask<'_>>) -> Option<Params> {
let to_shape = self.shape.to_local(self.scale)?;
let rect = self.shape.rect;
let (clip_rect, clip_radii, clip_curvature, to_clip) = match self.clip {
None => (NO_CLIP_RECT, [0.0; 4], 1.0, Transform::IDENTITY),
Some(clip) => match clip.to_local(self.scale) {
Some(to_clip) => {
let r = clip.rect;
(
[r.x, r.y, r.width, r.height],
clip.radii.to_array(),
clip.curvature,
to_clip,
)
}
None => (EMPTY_CLIP_RECT, [0.0; 4], 1.0, Transform::IDENTITY),
},
};
Some(Params {
curvature: self.shape.curvature,
radii: self.shape.radii.to_array(),
shape_rect: [rect.x, rect.y, rect.width, rect.height],
viewport_origin: [origin.0 as f32, origin.1 as f32],
to_shape: to_shape.data,
mask_size: mask.map_or([1.0, 1.0], |m| [m.size.0 as f32, m.size.1 as f32]),
clip_rect,
clip_radii,
clip_curvature,
to_clip: to_clip.data,
..Params::zeroed()
})
}
}
struct Targets {
width: u32,
height: u32,
work_width: u32,
work_height: u32,
#[allow(dead_code)]
scene: wgpu::Texture,
scene_view: wgpu::TextureView,
#[allow(dead_code)]
working: [wgpu::Texture; 2],
working_views: [wgpu::TextureView; 2],
}
pub struct BackdropRenderer {
format: wgpu::TextureFormat,
sampler: wgpu::Sampler,
bind_group_layout: wgpu::BindGroupLayout,
mask_bind_group_layout: wgpu::BindGroupLayout,
downsample: wgpu::RenderPipeline,
blur: wgpu::RenderPipeline,
composite: wgpu::RenderPipeline,
composite_mask: wgpu::RenderPipeline,
outline: wgpu::RenderPipeline,
blit: wgpu::RenderPipeline,
targets: Option<Targets>,
idle_frames: u32,
}
impl BackdropRenderer {
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Backdrop Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("backdrop_shader.wgsl").into()),
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Backdrop 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,
..Default::default()
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Backdrop Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
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 mask_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Backdrop Mask Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
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,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
],
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Backdrop Pipeline Layout"),
bind_group_layouts: &[&bind_group_layout],
immediate_size: 0,
});
let mask_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Backdrop Mask Pipeline Layout"),
bind_group_layouts: &[&mask_bind_group_layout],
immediate_size: 0,
});
let build = |label: &str,
entry: &str,
blend: Option<wgpu::BlendState>,
layout: &wgpu::PipelineLayout| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some(entry),
targets: &[Some(wgpu::ColorTargetState {
format,
blend,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
})
};
let pipeline = |label: &str, entry: &str, blend: Option<wgpu::BlendState>| {
build(label, entry, blend, &layout)
};
let composite_blend = 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,
},
};
Self {
format,
sampler,
downsample: pipeline("Backdrop Downsample", "fs_downsample", None),
blur: pipeline("Backdrop Blur", "fs_blur", None),
composite: pipeline("Backdrop Composite", "fs_composite", Some(composite_blend)),
composite_mask: build(
"Backdrop Composite Mask",
"fs_composite_mask",
Some(composite_blend),
&mask_layout,
),
outline: build(
"Text Outline",
"fs_outline",
Some(composite_blend),
&mask_layout,
),
blit: pipeline("Backdrop Blit", "fs_downsample", None),
bind_group_layout,
mask_bind_group_layout,
targets: None,
idle_frames: 0,
}
}
pub fn ensure_targets(&mut self, device: &wgpu::Device, width: u32, height: u32) {
let stale = match &self.targets {
Some(targets) => targets.width != width || targets.height != height,
None => true,
};
if stale {
self.targets = Some(Targets::new(device, self.format, width, height));
}
self.idle_frames = 0;
}
pub fn scene_view(&self) -> Option<&wgpu::TextureView> {
self.targets.as_ref().map(|targets| &targets.scene_view)
}
pub fn note_unused(&mut self) {
if self.targets.is_none() {
return;
}
self.idle_frames += 1;
if self.idle_frames > IDLE_FRAMES_BEFORE_RELEASE {
self.targets = None;
self.idle_frames = 0;
}
}
fn bind(
&self,
device: &wgpu::Device,
view: &wgpu::TextureView,
params: Params,
) -> wgpu::BindGroup {
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Backdrop Params"),
contents: bytemuck::cast_slice(&[params]),
usage: wgpu::BufferUsages::UNIFORM,
});
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Backdrop Bind Group"),
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: buffer.as_entire_binding(),
},
],
})
}
fn bind_masked(
&self,
device: &wgpu::Device,
view: &wgpu::TextureView,
mask: &wgpu::TextureView,
params: Params,
) -> wgpu::BindGroup {
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Backdrop Params"),
contents: bytemuck::cast_slice(&[params]),
usage: wgpu::BufferUsages::UNIFORM,
});
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Backdrop Mask Bind Group"),
layout: &self.mask_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: buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(mask),
},
],
})
}
#[allow(clippy::too_many_arguments)]
fn pass(
&self,
encoder: &mut wgpu::CommandEncoder,
pipeline: &wgpu::RenderPipeline,
target: &wgpu::TextureView,
load: wgpu::LoadOp<wgpu::Color>,
bind_group: &wgpu::BindGroup,
viewport: (u32, u32, u32, u32),
label: &str,
) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(label),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
ops: wgpu::Operations {
load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
let (x, y, w, h) = viewport;
pass.set_viewport(x as f32, y as f32, w as f32, h as f32, 0.0, 1.0);
pass.set_pipeline(pipeline);
pass.set_bind_group(0, bind_group, &[]);
pass.draw(0..3, 0..1);
}
pub fn apply(
&self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
region: &BackdropRegion,
) {
self.filter(device, encoder, region, None);
}
pub fn apply_masked(
&self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
region: &BackdropRegion,
mask: CoverageMask<'_>,
) {
self.filter(device, encoder, region, Some(mask));
}
pub fn apply_outline(
&self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
region: &BackdropRegion,
mask: CoverageMask<'_>,
color: Color,
width: f32,
) {
let Some(targets) = &self.targets else {
return;
};
let Some(viewport) = region.clamped(targets.width, targets.height) else {
return;
};
let (x, y, dst_width, dst_height) = viewport;
let params = Params {
src_rect: [0.0, 0.0, 1.0, 1.0],
dst_size: [dst_width as f32, dst_height as f32],
stroke_color: [color.r, color.g, color.b, color.a],
stroke_width: width,
..match region.shape_params((x, y), Some(mask)) {
Some(params) => params,
None => return,
}
};
let bind = self.bind_masked(device, &targets.working_views[0], mask.view, params);
self.pass(
encoder,
&self.outline,
&targets.scene_view,
wgpu::LoadOp::Load,
&bind,
viewport,
"Text Outline",
);
}
fn filter(
&self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
region: &BackdropRegion,
mask: Option<CoverageMask<'_>>,
) {
let Some(targets) = &self.targets else {
return;
};
let Some((x, y, width, height)) = region.clamped(targets.width, targets.height) else {
return;
};
let work_width = (width / WORKING_SCALE).max(1);
let work_height = (height / WORKING_SCALE).max(1);
let sigma = (region.radius * region.scale / WORKING_SCALE as f32 / 2.0).max(0.5);
let taps = (sigma * 2.5).ceil().min(MAX_TAPS);
let scene_rect = [
x as f32 / targets.width as f32,
y as f32 / targets.height as f32,
width as f32 / targets.width as f32,
height as f32 / targets.height as f32,
];
let work_rect = [
0.0,
0.0,
work_width as f32 / targets.work_width as f32,
work_height as f32 / targets.work_height as f32,
];
let base = Params {
src_rect: scene_rect,
sigma,
taps,
dst_size: [width as f32, height as f32],
..match region.shape_params((x, y), mask) {
Some(params) => params,
None => return,
}
};
let bind = self.bind(device, &targets.scene_view, base);
self.pass(
encoder,
&self.downsample,
&targets.working_views[0],
wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
&bind,
(0, 0, work_width, work_height),
"Backdrop Downsample",
);
let texel_x = 1.0 / targets.work_width as f32;
let texel_y = 1.0 / targets.work_height as f32;
for (index, direction) in [[texel_x, 0.0], [0.0, texel_y]].into_iter().enumerate() {
let bind = self.bind(
device,
&targets.working_views[index],
Params {
src_rect: work_rect,
direction,
..base
},
);
self.pass(
encoder,
&self.blur,
&targets.working_views[1 - index],
wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
&bind,
(0, 0, work_width, work_height),
"Backdrop Blur",
);
}
let params = Params {
src_rect: work_rect,
..base
};
let (pipeline, bind, label) = match mask {
Some(mask) => (
&self.composite_mask,
self.bind_masked(device, &targets.working_views[0], mask.view, params),
"Backdrop Composite Mask",
),
None => (
&self.composite,
self.bind(device, &targets.working_views[0], params),
"Backdrop Composite",
),
};
self.pass(
encoder,
pipeline,
&targets.scene_view,
wgpu::LoadOp::Load,
&bind,
(x, y, width, height),
label,
);
}
pub fn present(
&self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
) {
let Some(targets) = &self.targets else {
return;
};
let bind = self.bind(
device,
&targets.scene_view,
Params {
src_rect: [0.0, 0.0, 1.0, 1.0],
..Params::zeroed()
},
);
self.pass(
encoder,
&self.blit,
target,
wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
&bind,
(0, 0, targets.width, targets.height),
"Backdrop Blit",
);
}
}
impl Targets {
fn new(device: &wgpu::Device, format: wgpu::TextureFormat, width: u32, height: u32) -> Self {
let width = width.max(1);
let height = height.max(1);
let make = |label: &str, width: u32, height: u32| {
device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
})
};
let scene = make("Backdrop Scene", width, height);
let scene_view = scene.create_view(&Default::default());
let work_width = (width / WORKING_SCALE).max(1);
let work_height = (height / WORKING_SCALE).max(1);
let working = [
make("Backdrop Working 0", work_width, work_height),
make("Backdrop Working 1", work_width, work_height),
];
let working_views = [
working[0].create_view(&Default::default()),
working[1].create_view(&Default::default()),
];
Self {
width,
height,
work_width,
work_height,
scene,
scene_view,
working,
working_views,
}
}
}