1use crate::Surface;
6use backdrop_blur_core::{BackdropBlur, BlurError, BlurRequest, Region, RepaintPolicy, Scale};
7use backdrop_blur_wgpu::{SourceColorSpace, SourceView, WgpuBlur};
8
9impl Surface {
14 fn request(&self, pixels_per_point: f32) -> BlurRequest {
17 let origin = [
18 (self.rect.min.x * pixels_per_point).round().max(0.0) as u32,
19 (self.rect.min.y * pixels_per_point).round().max(0.0) as u32,
20 ];
21 let size = [
22 (self.rect.width() * pixels_per_point).round().max(0.0) as u32,
23 (self.rect.height() * pixels_per_point).round().max(0.0) as u32,
24 ];
25 let region = Region {
26 origin,
27 size,
28 scale: Scale::new(pixels_per_point),
29 };
30 BlurRequest {
31 source_region: region,
32 target_rect: region,
33 strength: self.strength,
34 tint: self.tint,
35 corner_radius: self.corner_radius,
36 opacity: self.opacity,
37 }
38 }
39}
40
41pub fn strongest_repaint(surfaces: &[Surface]) -> RepaintPolicy {
45 surfaces
46 .iter()
47 .fold(RepaintPolicy::Static, |acc, s| match (acc, s.repaint) {
48 (RepaintPolicy::Live, _) | (_, RepaintPolicy::Live) => RepaintPolicy::Live,
49 (RepaintPolicy::Bounded(a), RepaintPolicy::Bounded(b)) => {
50 RepaintPolicy::Bounded(a.min(b))
51 }
52 (RepaintPolicy::Bounded(d), RepaintPolicy::Static)
53 | (RepaintPolicy::Static, RepaintPolicy::Bounded(d)) => RepaintPolicy::Bounded(d),
54 (RepaintPolicy::Static, RepaintPolicy::Static) => RepaintPolicy::Static,
55 })
56}
57
58pub(crate) struct SeamContext<'a, B: BackdropBlur> {
61 pub device: &'a B::Device,
62 pub queue: &'a B::Queue,
63 pub encoder: &'a mut B::Encoder,
64 pub source: &'a B::SourceTexture,
65 pub target: &'a B::Target,
66 pub target_format: B::TargetFormat,
67}
68
69pub(crate) fn composite_surfaces<B>(
76 blur: &mut B,
77 ctx: SeamContext<'_, B>,
78 surfaces: &[Surface],
79 pixels_per_point: f32,
80) -> Result<usize, BlurError>
81where
82 B: BackdropBlur,
83 B::TargetFormat: Copy,
84{
85 let mut recorded = 0;
86 for surface in surfaces {
87 let request = surface.request(pixels_per_point);
88 if let Some(prepared) = blur.prepare(
89 ctx.device,
90 ctx.queue,
91 ctx.source,
92 ctx.target_format,
93 &request,
94 )? {
95 blur.record(ctx.encoder, ctx.target, &prepared)?;
96 recorded += 1;
97 }
98 }
99 Ok(recorded)
100}
101
102struct Intermediate {
105 texture: wgpu::Texture,
106 size: [u32; 2],
107}
108
109pub fn is_supported_target(format: wgpu::TextureFormat) -> bool {
115 matches!(
116 format,
117 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm
118 )
119}
120
121pub struct OwnLoopRenderer {
125 renderer: egui_wgpu::Renderer,
126 target_format: wgpu::TextureFormat,
127 intermediate: Option<Intermediate>,
128}
129
130impl OwnLoopRenderer {
131 pub fn new(
138 device: &wgpu::Device,
139 target_format: wgpu::TextureFormat,
140 ) -> Result<Self, BlurError> {
141 if !is_supported_target(target_format) {
142 return Err(BlurError::UnsupportedTarget {
143 format: format!("{target_format:?} (own-loop needs a non-sRGB Unorm target)"),
144 });
145 }
146 let renderer =
147 egui_wgpu::Renderer::new(device, target_format, egui_wgpu::RendererOptions::default());
148 Ok(Self {
149 renderer,
150 target_format,
151 intermediate: None,
152 })
153 }
154
155 fn intermediate(&mut self, device: &wgpu::Device, size: [u32; 2]) -> &Intermediate {
159 if self.intermediate.as_ref().is_none_or(|i| i.size != size) {
160 self.intermediate = None;
161 }
162 let format = self.target_format;
163 self.intermediate.get_or_insert_with(|| {
164 let texture = device.create_texture(&wgpu::TextureDescriptor {
165 label: Some("backdrop-blur egui intermediate"),
166 size: wgpu::Extent3d {
167 width: size[0].max(1),
168 height: size[1].max(1),
169 depth_or_array_layers: 1,
170 },
171 mip_level_count: 1,
172 sample_count: 1,
173 dimension: wgpu::TextureDimension::D2,
174 format,
175 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
176 | wgpu::TextureUsages::TEXTURE_BINDING,
177 view_formats: &[],
178 });
179 Intermediate { texture, size }
180 })
181 }
182
183 pub fn render_frame(
188 &mut self,
189 device: &wgpu::Device,
190 queue: &wgpu::Queue,
191 ctx: &egui::Context,
192 blur: &mut WgpuBlur,
193 frame: FrameInput<'_>,
194 surfaces: &[Surface],
195 ) -> Result<(), BlurError> {
196 for (id, delta) in &frame.textures_delta.set {
198 self.renderer.update_texture(device, queue, *id, delta);
199 }
200
201 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
202 label: Some("backdrop-blur own-loop frame"),
203 });
204
205 let egui_buffers = self.renderer.update_buffers(
208 device,
209 queue,
210 &mut encoder,
211 frame.paint_jobs,
212 &frame.screen,
213 );
214
215 let size = frame.screen.size_in_pixels;
218 let intermediate_view = self
219 .intermediate(device, size)
220 .texture
221 .create_view(&wgpu::TextureViewDescriptor::default());
222
223 {
227 let mut pass = begin_clear_pass(
228 &mut encoder,
229 &intermediate_view,
230 "backdrop-blur egui→intermediate",
231 );
232 self.renderer
233 .render(&mut pass, frame.paint_jobs, &frame.screen);
234 }
235 {
236 let mut pass =
237 begin_clear_pass(&mut encoder, frame.target, "backdrop-blur egui→target");
238 self.renderer
239 .render(&mut pass, frame.paint_jobs, &frame.screen);
240 }
241
242 let source = SourceView {
244 view: intermediate_view,
245 size,
246 color_space: SourceColorSpace::GammaSrgb,
247 };
248 composite_surfaces(
249 blur,
250 SeamContext {
251 device,
252 queue,
253 encoder: &mut encoder,
254 source: &source,
255 target: frame.target,
256 target_format: self.target_format,
257 },
258 surfaces,
259 frame.screen.pixels_per_point,
260 )?;
261
262 let main = encoder.finish();
264 queue.submit(egui_buffers.into_iter().chain(std::iter::once(main)));
265
266 for id in &frame.textures_delta.free {
268 self.renderer.free_texture(id);
269 }
270
271 match strongest_repaint(surfaces) {
273 RepaintPolicy::Live => ctx.request_repaint(),
274 RepaintPolicy::Bounded(after) => ctx.request_repaint_after(after),
275 RepaintPolicy::Static => {}
276 }
277 Ok(())
278 }
279}
280
281pub struct FrameInput<'a> {
283 pub target: &'a wgpu::TextureView,
285 pub paint_jobs: &'a [egui::ClippedPrimitive],
293 pub textures_delta: &'a egui::TexturesDelta,
295 pub screen: egui_wgpu::ScreenDescriptor,
297}
298
299fn begin_clear_pass(
302 encoder: &mut wgpu::CommandEncoder,
303 view: &wgpu::TextureView,
304 label: &str,
305) -> wgpu::RenderPass<'static> {
306 encoder
307 .begin_render_pass(&wgpu::RenderPassDescriptor {
308 label: Some(label),
309 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
310 view,
311 resolve_target: None,
312 depth_slice: None,
313 ops: wgpu::Operations {
314 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
315 store: wgpu::StoreOp::Store,
316 },
317 })],
318 depth_stencil_attachment: None,
319 timestamp_writes: None,
320 occlusion_query_set: None,
321 multiview_mask: None,
322 })
323 .forget_lifetime()
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
334 use backdrop_blur_core::{BlurStrength, CornerRadius, Tint};
335 use std::cell::RefCell;
336
337 #[test]
338 fn is_supported_target_accepts_only_non_srgb_unorm() {
339 assert!(is_supported_target(wgpu::TextureFormat::Rgba8Unorm));
340 assert!(is_supported_target(wgpu::TextureFormat::Bgra8Unorm));
341 assert!(!is_supported_target(wgpu::TextureFormat::Rgba8UnormSrgb));
343 assert!(!is_supported_target(wgpu::TextureFormat::Bgra8UnormSrgb));
344 assert!(!is_supported_target(wgpu::TextureFormat::Rgba16Float));
345 }
346
347 #[derive(Default)]
352 struct RecordingBlur {
353 events: RefCell<Vec<&'static str>>,
354 }
355
356 impl BackdropBlur for RecordingBlur {
357 type Device = ();
358 type Queue = ();
359 type Encoder = ();
360 type SourceTexture = ();
361 type Target = ();
362 type TargetFormat = ();
363 type Prepared = ();
364
365 fn prepare(
366 &mut self,
367 _device: &(),
368 _queue: &(),
369 _source: &(),
370 _target_format: (),
371 request: &BlurRequest,
372 ) -> Result<Option<()>, BlurError> {
373 self.events.borrow_mut().push("prepare");
374 if request.source_region.size[0] == 0 || request.source_region.size[1] == 0 {
375 Ok(None)
376 } else {
377 Ok(Some(()))
378 }
379 }
380
381 fn record(&self, _encoder: &mut (), _target: &(), _prepared: &()) -> Result<(), BlurError> {
382 self.events.borrow_mut().push("record");
383 Ok(())
384 }
385 }
386
387 fn surface(rect: egui::Rect) -> Surface {
388 Surface {
389 rect,
390 strength: BlurStrength::new(8.0),
391 tint: Tint::new(backdrop_blur_core::LinearRgba::new(0.0, 0.0, 0.0, 0.1)),
392 corner_radius: CornerRadius::new(12.0),
393 opacity: backdrop_blur_core::Opacity::default(),
394 repaint: RepaintPolicy::Static,
395 }
396 }
397
398 #[test]
399 fn composite_surfaces_prepares_each_and_records_only_non_empty() {
400 let mut blur = RecordingBlur::default();
401 let surfaces = [
402 surface(egui::Rect::from_min_size(
403 egui::pos2(10.0, 10.0),
404 egui::vec2(100.0, 60.0),
405 )),
406 surface(egui::Rect::from_min_size(
407 egui::pos2(0.0, 0.0),
408 egui::vec2(0.0, 0.0),
409 )), surface(egui::Rect::from_min_size(
411 egui::pos2(50.0, 50.0),
412 egui::vec2(80.0, 40.0),
413 )),
414 ];
415 let recorded = composite_surfaces(
416 &mut blur,
417 SeamContext {
418 device: &(),
419 queue: &(),
420 encoder: &mut (),
421 source: &(),
422 target: &(),
423 target_format: (),
424 },
425 &surfaces,
426 1.0,
427 )
428 .expect("the fake backend never errors");
429
430 assert_eq!(recorded, 2);
431 let events = blur.events.into_inner();
432 assert_eq!(
433 events.iter().filter(|e| **e == "prepare").count(),
434 3,
435 "prepare runs for every surface"
436 );
437 assert_eq!(
438 events.iter().filter(|e| **e == "record").count(),
439 2,
440 "record skips the empty surface"
441 );
442 assert_eq!(
444 events,
445 vec!["prepare", "record", "prepare", "prepare", "record"]
446 );
447 }
448
449 #[test]
450 fn strongest_repaint_prefers_live_then_shortest_bounded() {
451 use std::time::Duration;
452 let live = surface(egui::Rect::ZERO);
453 let mut live = live;
454 live.repaint = RepaintPolicy::Live;
455
456 let mut bounded_long = surface(egui::Rect::ZERO);
457 bounded_long.repaint = RepaintPolicy::Bounded(Duration::from_millis(500));
458 let mut bounded_short = surface(egui::Rect::ZERO);
459 bounded_short.repaint = RepaintPolicy::Bounded(Duration::from_millis(100));
460
461 assert_eq!(strongest_repaint(&[]), RepaintPolicy::Static);
462 assert_eq!(
463 strongest_repaint(&[bounded_long, bounded_short]),
464 RepaintPolicy::Bounded(Duration::from_millis(100))
465 );
466 assert_eq!(
467 strongest_repaint(&[bounded_long, live]),
468 RepaintPolicy::Live
469 );
470 }
471}