use crate::Surface;
use backdrop_blur_core::{BackdropBlur, BlurError, BlurRequest, Region, RepaintPolicy, Scale};
use backdrop_blur_wgpu::{SourceColorSpace, SourceView, WgpuBlur};
impl Surface {
fn request(&self, pixels_per_point: f32) -> BlurRequest {
let origin = [
(self.rect.min.x * pixels_per_point).round().max(0.0) as u32,
(self.rect.min.y * pixels_per_point).round().max(0.0) as u32,
];
let size = [
(self.rect.width() * pixels_per_point).round().max(0.0) as u32,
(self.rect.height() * pixels_per_point).round().max(0.0) as u32,
];
let region = Region {
origin,
size,
scale: Scale::new(pixels_per_point),
};
BlurRequest {
source_region: region,
target_rect: region,
strength: self.strength,
tint: self.tint,
corner_radius: self.corner_radius,
opacity: self.opacity,
}
}
}
pub fn strongest_repaint(surfaces: &[Surface]) -> RepaintPolicy {
surfaces
.iter()
.fold(RepaintPolicy::Static, |acc, s| match (acc, s.repaint) {
(RepaintPolicy::Live, _) | (_, RepaintPolicy::Live) => RepaintPolicy::Live,
(RepaintPolicy::Bounded(a), RepaintPolicy::Bounded(b)) => {
RepaintPolicy::Bounded(a.min(b))
}
(RepaintPolicy::Bounded(d), RepaintPolicy::Static)
| (RepaintPolicy::Static, RepaintPolicy::Bounded(d)) => RepaintPolicy::Bounded(d),
(RepaintPolicy::Static, RepaintPolicy::Static) => RepaintPolicy::Static,
})
}
pub(crate) struct SeamContext<'a, B: BackdropBlur> {
pub device: &'a B::Device,
pub queue: &'a B::Queue,
pub encoder: &'a mut B::Encoder,
pub source: &'a B::SourceTexture,
pub target: &'a B::Target,
pub target_format: B::TargetFormat,
}
pub(crate) fn composite_surfaces<B>(
blur: &mut B,
ctx: SeamContext<'_, B>,
surfaces: &[Surface],
pixels_per_point: f32,
) -> Result<usize, BlurError>
where
B: BackdropBlur,
B::TargetFormat: Copy,
{
let mut recorded = 0;
for surface in surfaces {
let request = surface.request(pixels_per_point);
if let Some(prepared) = blur.prepare(
ctx.device,
ctx.queue,
ctx.source,
ctx.target_format,
&request,
)? {
blur.record(ctx.encoder, ctx.target, &prepared)?;
recorded += 1;
}
}
Ok(recorded)
}
struct Intermediate {
texture: wgpu::Texture,
size: [u32; 2],
}
pub fn is_supported_target(format: wgpu::TextureFormat) -> bool {
matches!(
format,
wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm
)
}
pub struct OwnLoopRenderer {
renderer: egui_wgpu::Renderer,
target_format: wgpu::TextureFormat,
intermediate: Option<Intermediate>,
}
impl OwnLoopRenderer {
pub fn new(
device: &wgpu::Device,
target_format: wgpu::TextureFormat,
) -> Result<Self, BlurError> {
if !is_supported_target(target_format) {
return Err(BlurError::UnsupportedTarget {
format: format!("{target_format:?} (own-loop needs a non-sRGB Unorm target)"),
});
}
let renderer =
egui_wgpu::Renderer::new(device, target_format, egui_wgpu::RendererOptions::default());
Ok(Self {
renderer,
target_format,
intermediate: None,
})
}
fn intermediate(&mut self, device: &wgpu::Device, size: [u32; 2]) -> &Intermediate {
if self.intermediate.as_ref().is_none_or(|i| i.size != size) {
self.intermediate = None;
}
let format = self.target_format;
self.intermediate.get_or_insert_with(|| {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("backdrop-blur egui intermediate"),
size: wgpu::Extent3d {
width: size[0].max(1),
height: size[1].max(1),
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: &[],
});
Intermediate { texture, size }
})
}
pub fn render_frame(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
ctx: &egui::Context,
blur: &mut WgpuBlur,
frame: FrameInput<'_>,
surfaces: &[Surface],
) -> Result<(), BlurError> {
for (id, delta) in &frame.textures_delta.set {
self.renderer.update_texture(device, queue, *id, delta);
}
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("backdrop-blur own-loop frame"),
});
let egui_buffers = self.renderer.update_buffers(
device,
queue,
&mut encoder,
frame.paint_jobs,
&frame.screen,
);
let size = frame.screen.size_in_pixels;
let intermediate_view = self
.intermediate(device, size)
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
{
let mut pass = begin_clear_pass(
&mut encoder,
&intermediate_view,
"backdrop-blur egui→intermediate",
);
self.renderer
.render(&mut pass, frame.paint_jobs, &frame.screen);
}
{
let mut pass =
begin_clear_pass(&mut encoder, frame.target, "backdrop-blur egui→target");
self.renderer
.render(&mut pass, frame.paint_jobs, &frame.screen);
}
let source = SourceView {
view: intermediate_view,
size,
color_space: SourceColorSpace::GammaSrgb,
};
composite_surfaces(
blur,
SeamContext {
device,
queue,
encoder: &mut encoder,
source: &source,
target: frame.target,
target_format: self.target_format,
},
surfaces,
frame.screen.pixels_per_point,
)?;
let main = encoder.finish();
queue.submit(egui_buffers.into_iter().chain(std::iter::once(main)));
for id in &frame.textures_delta.free {
self.renderer.free_texture(id);
}
match strongest_repaint(surfaces) {
RepaintPolicy::Live => ctx.request_repaint(),
RepaintPolicy::Bounded(after) => ctx.request_repaint_after(after),
RepaintPolicy::Static => {}
}
Ok(())
}
}
pub struct FrameInput<'a> {
pub target: &'a wgpu::TextureView,
pub paint_jobs: &'a [egui::ClippedPrimitive],
pub textures_delta: &'a egui::TexturesDelta,
pub screen: egui_wgpu::ScreenDescriptor,
}
fn begin_clear_pass(
encoder: &mut wgpu::CommandEncoder,
view: &wgpu::TextureView,
label: &str,
) -> wgpu::RenderPass<'static> {
encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(label),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
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,
})
.forget_lifetime()
}
#[cfg(test)]
mod tests {
use super::*;
use backdrop_blur_core::{BlurStrength, CornerRadius, Tint};
use std::cell::RefCell;
#[test]
fn is_supported_target_accepts_only_non_srgb_unorm() {
assert!(is_supported_target(wgpu::TextureFormat::Rgba8Unorm));
assert!(is_supported_target(wgpu::TextureFormat::Bgra8Unorm));
assert!(!is_supported_target(wgpu::TextureFormat::Rgba8UnormSrgb));
assert!(!is_supported_target(wgpu::TextureFormat::Bgra8UnormSrgb));
assert!(!is_supported_target(wgpu::TextureFormat::Rgba16Float));
}
#[derive(Default)]
struct RecordingBlur {
events: RefCell<Vec<&'static str>>,
}
impl BackdropBlur for RecordingBlur {
type Device = ();
type Queue = ();
type Encoder = ();
type SourceTexture = ();
type Target = ();
type TargetFormat = ();
type Prepared = ();
fn prepare(
&mut self,
_device: &(),
_queue: &(),
_source: &(),
_target_format: (),
request: &BlurRequest,
) -> Result<Option<()>, BlurError> {
self.events.borrow_mut().push("prepare");
if request.source_region.size[0] == 0 || request.source_region.size[1] == 0 {
Ok(None)
} else {
Ok(Some(()))
}
}
fn record(&self, _encoder: &mut (), _target: &(), _prepared: &()) -> Result<(), BlurError> {
self.events.borrow_mut().push("record");
Ok(())
}
}
fn surface(rect: egui::Rect) -> Surface {
Surface {
rect,
strength: BlurStrength::new(8.0),
tint: Tint::new(backdrop_blur_core::LinearRgba::new(0.0, 0.0, 0.0, 0.1)),
corner_radius: CornerRadius::new(12.0),
opacity: backdrop_blur_core::Opacity::default(),
repaint: RepaintPolicy::Static,
}
}
#[test]
fn composite_surfaces_prepares_each_and_records_only_non_empty() {
let mut blur = RecordingBlur::default();
let surfaces = [
surface(egui::Rect::from_min_size(
egui::pos2(10.0, 10.0),
egui::vec2(100.0, 60.0),
)),
surface(egui::Rect::from_min_size(
egui::pos2(0.0, 0.0),
egui::vec2(0.0, 0.0),
)), surface(egui::Rect::from_min_size(
egui::pos2(50.0, 50.0),
egui::vec2(80.0, 40.0),
)),
];
let recorded = composite_surfaces(
&mut blur,
SeamContext {
device: &(),
queue: &(),
encoder: &mut (),
source: &(),
target: &(),
target_format: (),
},
&surfaces,
1.0,
)
.expect("the fake backend never errors");
assert_eq!(recorded, 2);
let events = blur.events.into_inner();
assert_eq!(
events.iter().filter(|e| **e == "prepare").count(),
3,
"prepare runs for every surface"
);
assert_eq!(
events.iter().filter(|e| **e == "record").count(),
2,
"record skips the empty surface"
);
assert_eq!(
events,
vec!["prepare", "record", "prepare", "prepare", "record"]
);
}
#[test]
fn strongest_repaint_prefers_live_then_shortest_bounded() {
use std::time::Duration;
let live = surface(egui::Rect::ZERO);
let mut live = live;
live.repaint = RepaintPolicy::Live;
let mut bounded_long = surface(egui::Rect::ZERO);
bounded_long.repaint = RepaintPolicy::Bounded(Duration::from_millis(500));
let mut bounded_short = surface(egui::Rect::ZERO);
bounded_short.repaint = RepaintPolicy::Bounded(Duration::from_millis(100));
assert_eq!(strongest_repaint(&[]), RepaintPolicy::Static);
assert_eq!(
strongest_repaint(&[bounded_long, bounded_short]),
RepaintPolicy::Bounded(Duration::from_millis(100))
);
assert_eq!(
strongest_repaint(&[bounded_long, live]),
RepaintPolicy::Live
);
}
}