diff --git a/crates/egui-wgpu/src/capture.rs b/crates/egui-wgpu/src/capture.rs index c5519e8a6..38ed56918 100644 --- a/crates/egui-wgpu/src/capture.rs +++ b/crates/egui-wgpu/src/capture.rs @@ -117,12 +117,19 @@ impl CaptureState { output_frame: &wgpu::SurfaceTexture, encoder: &mut wgpu::CommandEncoder, ) -> wgpu::Buffer { - debug_assert_eq!( - self.texture.size(), - output_frame.texture.size(), - "Texture sizes must match, `CaptureState::update` was probably not called" - ); + let buffer = self.copy_to_buffer(device, encoder); + self.blit_to_surface(output_frame, encoder); + buffer + } + /// Copies the [`CaptureState`] texture into a new buffer, ready to be read back to the cpu. + /// + /// Pass the returned buffer to [`CaptureState::read_screen_rgba`]. + pub fn copy_to_buffer( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + ) -> wgpu::Buffer { // It would be more efficient to reuse the Buffer, e.g. via some kind of ring buffer, but // for most screenshot use cases this should be fine. When taking many screenshots (e.g. for a video) // it might make sense to revisit this and implement a more efficient solution. @@ -151,6 +158,25 @@ impl CaptureState { tex_extent, ); + buffer + } + + /// Draws the [`CaptureState`] texture onto the surface texture. + /// + /// Needed whenever egui was rendered into this texture instead of straight into the + /// surface. That is the case both when capturing a screenshot and when a paint callback + /// asked for a backdrop, since the surface texture cannot be read on every platform. + pub fn blit_to_surface( + &self, + output_frame: &wgpu::SurfaceTexture, + encoder: &mut wgpu::CommandEncoder, + ) { + debug_assert_eq!( + self.texture.size(), + output_frame.texture.size(), + "Texture sizes must match, `CaptureState::update` was probably not called" + ); + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("texture_copy"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -171,8 +197,6 @@ impl CaptureState { pass.set_pipeline(&self.pipeline); pass.set_bind_group(0, &self.bind_group, &[]); pass.draw(0..3, 0..1); - - buffer } /// Handles copying from the [`CaptureState`] texture to the surface texture and the cpu diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 2ff9f7e4a..23d7fd641 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -117,6 +117,162 @@ pub trait CallbackTrait: Send + Sync { render_pass: &mut wgpu::RenderPass<'static>, callback_resources: &CallbackResources, ); + + /// Do you need to see what egui has already drawn underneath you? + /// + /// Return `true` for effects that transform the background rather than draw over it, + /// such as a blur behind a window. You then get [`CallbackTrait::process_backdrop`] + /// and [`CallbackTrait::paint_with_backdrop`] instead of [`CallbackTrait::paint`]. + /// + /// Because nothing can sample the texture it is currently drawing into, this makes + /// egui interrupt its render pass and copy the half-drawn frame aside, which is not + /// free. Only ask when you need it. + /// + /// This only works if the integration renders egui into its own texture rather than + /// straight into the window. `eframe` does that automatically as soon as one callback + /// asks for a backdrop; other integrations need to follow + /// [`Renderer::render_from`]'s instructions. If the integration does not support it, + /// [`CallbackTrait::paint`] is called as usual and no backdrop is captured. + fn needs_backdrop(&self) -> bool { + false + } + + /// Called between render passes, once the backdrop has been captured. + /// + /// You have the [`wgpu::CommandEncoder`], so this is where to run your own render + /// passes over the backdrop: blur it, tint it, distort it. Put the result somewhere + /// [`CallbackTrait::paint_with_backdrop`] can find it, such as a texture you allocated + /// in [`CallbackTrait::prepare`]. + /// + /// Only called when [`CallbackTrait::needs_backdrop`] returns `true`. + fn process_backdrop( + &self, + _device: &wgpu::Device, + _queue: &wgpu::Queue, + _egui_encoder: &mut wgpu::CommandEncoder, + _callback_resources: &CallbackResources, + _backdrop: &Backdrop<'_>, + ) { + } + + /// Called instead of [`CallbackTrait::paint`] when [`CallbackTrait::needs_backdrop`] + /// returns `true` and the integration was able to capture a backdrop. + /// + /// Draw into the render pass as you would in [`CallbackTrait::paint`], sampling + /// `backdrop` or whatever [`CallbackTrait::process_backdrop`] produced from it. + fn paint_with_backdrop( + &self, + info: PaintCallbackInfo, + render_pass: &mut wgpu::RenderPass<'static>, + callback_resources: &CallbackResources, + _backdrop: &Backdrop<'_>, + ) { + self.paint(info, render_pass, callback_resources); + } +} + +/// How far [`Renderer::render_from`] has got through the paint jobs. +/// +/// Start with [`RenderCursor::default`] and hand the same one back on each call. +#[derive(Clone, Copy, Debug)] +pub struct RenderCursor { + /// The paint job to draw next. + job: usize, + + /// How many meshes have been drawn, which is how far into the vertex and index buffer + /// slices we are. + mesh: usize, + + /// Whether the caller is able to capture backdrops. When it is not, we never stop for + /// one, and callbacks that wanted a backdrop are drawn without. + backdrops_supported: bool, +} + +impl Default for RenderCursor { + fn default() -> Self { + Self { + job: 0, + mesh: 0, + backdrops_supported: true, + } + } +} + +/// Whether [`Renderer::render_from`] finished, or stopped to let you capture a backdrop. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RenderProgress { + /// Every paint job has been drawn. + Done, + + /// A callback needs to see what is underneath it. + /// + /// End the render pass, call [`Renderer::capture_backdrop`], then call + /// [`Renderer::render_from`] again with a fresh pass that loads rather than clears. + NeedsBackdrop, +} + +/// A copy of everything egui had drawn when a backdrop effect was reached. +/// +/// Holds premultiplied alpha, like everything else egui renders. +#[derive(Clone, Copy)] +pub struct Backdrop<'a> { + /// The captured image, ready to sample. + pub view: &'a wgpu::TextureView, + + /// The size of [`Self::view`], in physical pixels. + pub size_in_pixels: [u32; 2], + + /// The format of [`Self::view`]. + pub format: wgpu::TextureFormat, +} + +/// Somewhere to keep the half-drawn frame while a backdrop effect reads it. +/// +/// Integrations that want to support [`CallbackTrait::needs_backdrop`] own one of these +/// and hand it to [`Renderer::capture_backdrop`]. +pub struct BackdropTexture { + texture: wgpu::Texture, + view: wgpu::TextureView, +} + +impl BackdropTexture { + /// Allocate a backdrop texture of the given size and format. + /// + /// The format must match the texture egui is being rendered into. + pub fn new(device: &wgpu::Device, size_in_pixels: [u32; 2], format: wgpu::TextureFormat) -> Self { + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("egui_backdrop"), + size: wgpu::Extent3d { + width: size_in_pixels[0].max(1), + height: size_in_pixels[1].max(1), + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[format], + }); + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + Self { texture, view } + } + + /// Reallocate if the size or format no longer matches. + pub fn update(&mut self, device: &wgpu::Device, size_in_pixels: [u32; 2], format: wgpu::TextureFormat) { + let size = self.texture.size(); + if size.width != size_in_pixels[0].max(1) + || size.height != size_in_pixels[1].max(1) + || self.texture.format() != format + { + *self = Self::new(device, size_in_pixels, format); + } + } + + /// The captured image. + pub fn view(&self) -> &wgpu::TextureView { + &self.view + } } /// Information about the screen used for rendering. @@ -479,6 +635,62 @@ impl Renderer { paint_jobs: &[epaint::ClippedPrimitive], screen_descriptor: &ScreenDescriptor, ) { + // This entry point cannot interrupt the render pass, since it does not own it, so + // tell `render_from` not to stop for backdrops. Callbacks that wanted one are + // drawn with a plain `paint` instead. + let mut cursor = RenderCursor { + backdrops_supported: false, + ..RenderCursor::default() + }; + self.render_from(render_pass, paint_jobs, screen_descriptor, &mut cursor, None); + } + + /// Draw paint jobs, stopping when one of them needs a backdrop. + /// + /// Use this instead of [`Renderer::render`] to support + /// [`CallbackTrait::needs_backdrop`]. Nothing can sample the texture it is currently + /// drawing into, so a backdrop effect needs the render pass to be interrupted and the + /// half-drawn frame copied aside. That means the integration, which owns the render + /// pass, has to drive the loop: + /// + /// ```ignore + /// let mut cursor = RenderCursor::default(); + /// let mut load = wgpu::LoadOp::Clear(clear_color); + /// loop { + /// let mut pass = begin_render_pass(&mut encoder, load).forget_lifetime(); + /// let progress = renderer.render_from( + /// &mut pass, &jobs, &screen_descriptor, &mut cursor, backdrop.as_ref(), + /// ); + /// drop(pass); // ends the pass, so the target can be read + /// if progress == RenderProgress::Done { + /// break; + /// } + /// renderer.capture_backdrop( + /// device, queue, &mut encoder, &jobs, cursor, + /// &target_texture, &mut backdrop_texture, + /// ); + /// load = wgpu::LoadOp::Load; // keep what we have already drawn + /// } + /// ``` + /// + /// `target_texture` must be a texture the integration owns, with + /// [`wgpu::TextureUsages::COPY_SRC`], rather than the window's surface texture, which + /// cannot be copied from on every platform. Blit it to the surface at the end. + /// + /// Pass the `backdrop` from the previous [`Renderer::capture_backdrop`] call, or + /// `None` on the first pass. If you pass `None` for a callback that asked for a + /// backdrop, it is drawn with [`CallbackTrait::paint`] instead. + /// + /// # Panic + /// Always ensure that [`Renderer::update_buffers`] has been called first. + pub fn render_from( + &self, + render_pass: &mut wgpu::RenderPass<'static>, + paint_jobs: &[epaint::ClippedPrimitive], + screen_descriptor: &ScreenDescriptor, + cursor: &mut RenderCursor, + backdrop: Option<&Backdrop<'_>>, + ) -> RenderProgress { profiling::function_scope!(); let pixels_per_point = screen_descriptor.pixels_per_point; @@ -488,14 +700,34 @@ impl Renderer { // run. let mut needs_reset = true; - let mut index_buffer_slices = self.index_buffer.slices.iter(); - let mut vertex_buffer_slices = self.vertex_buffer.slices.iter(); + let mut index_buffer_slices = self.index_buffer.slices.iter().skip(cursor.mesh); + let mut vertex_buffer_slices = self.vertex_buffer.slices.iter().skip(cursor.mesh); - for epaint::ClippedPrimitive { + // The backdrop, if any, belongs to the callback we stopped at last time, which is + // the one the cursor now points at. Anything after that has to capture its own. + let mut backdrop = backdrop; + + while let Some(epaint::ClippedPrimitive { clip_rect, primitive, - } in paint_jobs + }) = paint_jobs.get(cursor.job) { + if cursor.backdrops_supported + && backdrop.is_none() + && let Primitive::Callback(callback) = primitive + && let Some(cbfn) = callback.callback.downcast_ref::() + && cbfn.0.needs_backdrop() + { + // Stop here and let the caller end the pass and capture the backdrop. The + // cursor stays on this job, so we draw it when we are called again. + return RenderProgress::NeedsBackdrop; + } + + if let Primitive::Mesh(_) = primitive { + cursor.mesh += 1; + } + cursor.job += 1; + if needs_reset { render_pass.set_viewport( 0.0, @@ -594,13 +826,108 @@ impl Renderer { 1.0, ); - cbfn.0.paint(info, render_pass, &self.callback_resources); + // `backdrop` was captured for this callback and this callback only. + // Taking it means a later backdrop effect stops the pass again to + // capture its own, which it must: by then we will have drawn this + // one, and it needs to see that. + match backdrop.take().filter(|_| cbfn.0.needs_backdrop()) { + Some(backdrop) => cbfn.0.paint_with_backdrop( + info, + render_pass, + &self.callback_resources, + backdrop, + ), + None => cbfn.0.paint(info, render_pass, &self.callback_resources), + } } } } } render_pass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]); + RenderProgress::Done + } + + /// Copy the half-drawn frame aside so a backdrop effect can read it. + /// + /// Call this after ending the render pass, when [`Renderer::render_from`] returned + /// [`RenderProgress::NeedsBackdrop`]. It copies `target` into `backdrop` and then + /// lets the waiting callback run its own passes over it via + /// [`CallbackTrait::process_backdrop`]. + /// + /// `target` is the texture egui is being drawn into, and needs + /// [`wgpu::TextureUsages::COPY_SRC`]. With multisampling, pass the resolve target + /// rather than the multisampled texture. + /// + /// `backdrop` must already be the same size and format as `target`; call + /// [`BackdropTexture::update`] before the loop starts. + pub fn capture_backdrop<'a>( + &self, + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + paint_jobs: &[epaint::ClippedPrimitive], + cursor: RenderCursor, + target: &wgpu::Texture, + backdrop: &'a BackdropTexture, + ) -> Option> { + profiling::function_scope!(); + + let Some(epaint::ClippedPrimitive { + primitive: Primitive::Callback(callback), + .. + }) = paint_jobs.get(cursor.job) + else { + debug_assert!( + false, + "Bug in egui-wgpu: capture_backdrop was called when the cursor was not on a callback" + ); + return None; + }; + let cbfn = callback.callback.downcast_ref::()?; + + let size = target.size(); + let size_in_pixels = [size.width, size.height]; + let format = target.format(); + debug_assert_eq!( + backdrop.texture.size(), + size, + "The backdrop texture must be the same size as the texture egui is drawn into; call `BackdropTexture::update`" + ); + debug_assert_eq!( + backdrop.texture.format(), + format, + "The backdrop texture must be the same format as the texture egui is drawn into; call `BackdropTexture::update`" + ); + + encoder.copy_texture_to_texture( + target.as_image_copy(), + backdrop.texture.as_image_copy(), + size, + ); + + let backdrop = Backdrop { + view: &backdrop.view, + size_in_pixels, + format, + }; + cbfn.0 + .process_backdrop(device, queue, encoder, &self.callback_resources, &backdrop); + Some(backdrop) + } + + /// Does any of these paint jobs want a backdrop? + /// + /// Integrations use this to decide whether to render egui into their own texture, which + /// backdrop effects need, instead of straight into the window's surface. + pub fn needs_backdrop(paint_jobs: &[epaint::ClippedPrimitive]) -> bool { + paint_jobs.iter().any(|job| { + matches!(&job.primitive, Primitive::Callback(callback) + if callback + .callback + .downcast_ref::() + .is_some_and(|cbfn| cbfn.0.needs_backdrop())) + }) } /// Should be called before [`Self::render`]. diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 62b5d9197..f13fb7e4b 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -5,12 +5,22 @@ use crate::{RenderState, SurfaceConfig, SurfaceErrorAction, WgpuConfiguration, renderer}; use crate::{ - RendererOptions, + BackdropTexture, RenderCursor, RenderProgress, Renderer, RendererOptions, capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel}, }; use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet}; use std::{num::NonZeroU32, sync::Arc}; +/// Clear the depth or stencil buffer on the first render pass of a frame, and keep it on +/// any later ones, matching what the color attachment is doing. +fn keep_or_clear(color_load: wgpu::LoadOp, clear_value: T) -> wgpu::LoadOp { + if matches!(color_load, wgpu::LoadOp::Load) { + wgpu::LoadOp::Load + } else { + wgpu::LoadOp::Clear(clear_value) + } +} + struct SurfaceState { surface: wgpu::Surface<'static>, alpha_mode: wgpu::CompositeAlphaMode, @@ -33,6 +43,9 @@ pub struct Painter { support_transparent_backbuffer: bool, screen_capture_state: Option, + /// Somewhere to keep the half-drawn frame while a backdrop effect reads it. + backdrop_texture: Option, + instance: wgpu::Instance, render_state: Option, @@ -72,6 +85,7 @@ impl Painter { options, support_transparent_backbuffer, screen_capture_state: None, + backdrop_texture: None, instance, render_state: None, @@ -630,18 +644,46 @@ impl Painter { { let renderer = render_state.renderer.read(); - let target_texture = if capture { + // Backdrop effects have to read what egui has already drawn, and the surface + // texture cannot be read on every platform, so render into our own texture and + // blit it onto the surface afterwards. That is the same thing a screenshot + // needs, so reuse the machinery. + let needs_backdrop = Renderer::needs_backdrop(clipped_primitives); + let render_to_own_texture = capture || needs_backdrop; + + if render_to_own_texture { let capture_state = self.screen_capture_state.get_or_insert_with(|| { CaptureState::new(&render_state.device, &output_frame.texture) }); capture_state.update(&render_state.device, &output_frame.texture); - - &capture_state.texture - } else { - &output_frame.texture - }; + } + let target_texture = self + .screen_capture_state + .as_ref() + .filter(|_| render_to_own_texture) + .map_or(&output_frame.texture, |capture_state| { + &capture_state.texture + }); let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default()); + // Size the backdrop texture up front, so that the borrow of it can be held for + // the whole render loop below. + if needs_backdrop { + let size = [target_texture.width(), target_texture.height()]; + let format = target_texture.format(); + match &mut self.backdrop_texture { + Some(backdrop) => backdrop.update(&render_state.device, size, format), + None => { + self.backdrop_texture = Some(BackdropTexture::new( + &render_state.device, + size, + format, + )); + } + } + } + let backdrop_texture = self.backdrop_texture.as_ref().filter(|_| needs_backdrop); + let (view, resolve_target) = (self.options.msaa_samples > 1) .then_some(self.msaa_texture_view.get(&viewport_id)) .flatten() @@ -649,69 +691,114 @@ impl Painter { (texture_view, Some(&target_view)) }); - let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("egui_render"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view, - resolve_target, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: clear_color[0] as f64, - g: clear_color[1] as f64, - b: clear_color[2] as f64, - a: clear_color[3] as f64, - }), - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - })], - depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| { - wgpu::RenderPassDepthStencilAttachment { - view, - depth_ops: self - .options - .depth_stencil_format - .is_some_and(|depth_stencil_format| { - depth_stencil_format.has_depth_aspect() - }) - .then_some(wgpu::Operations { - load: wgpu::LoadOp::Clear(1.0), - // It is very unlikely that the depth buffer is needed after egui finished rendering - // so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon) - store: wgpu::StoreOp::Discard, - }), - stencil_ops: self - .options - .depth_stencil_format - .is_some_and(|depth_stencil_format| { - depth_stencil_format.has_stencil_aspect() - }) - .then_some(wgpu::Operations { - load: wgpu::LoadOp::Clear(0), - store: wgpu::StoreOp::Discard, - }), - } - }), - timestamp_writes: None, - occlusion_query_set: None, - multiview_mask: None, + // Usually one pass is enough. A backdrop effect needs to read what egui has + // drawn so far, and nothing can read the texture it is drawing into, so the + // pass has to be ended and a new one started for each of those. + let mut cursor = RenderCursor::default(); + let mut backdrop = None; + let mut color_load = wgpu::LoadOp::Clear(wgpu::Color { + r: clear_color[0] as f64, + g: clear_color[1] as f64, + b: clear_color[2] as f64, + a: clear_color[3] as f64, }); - // Forgetting the pass' lifetime means that we are no longer compile-time protected from - // runtime errors caused by accessing the parent encoder before the render pass is dropped. - // Since we don't pass it on to the renderer, we should be perfectly safe against this mistake here! - renderer.render( - &mut render_pass.forget_lifetime(), - clipped_primitives, - &screen_descriptor, - ); + loop { + let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("egui_render"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + resolve_target, + ops: wgpu::Operations { + load: color_load, + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map( + |view| wgpu::RenderPassDepthStencilAttachment { + view, + depth_ops: self + .options + .depth_stencil_format + .is_some_and(|depth_stencil_format| { + depth_stencil_format.has_depth_aspect() + }) + .then_some(wgpu::Operations { + load: keep_or_clear(color_load, 1.0), + // It is very unlikely that the depth buffer is needed after egui finished rendering + // so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon). + // Backdrop effects split the render pass, and the later passes load what the earlier ones wrote. + store: if needs_backdrop { + wgpu::StoreOp::Store + } else { + wgpu::StoreOp::Discard + }, + }), + stencil_ops: self + .options + .depth_stencil_format + .is_some_and(|depth_stencil_format| { + depth_stencil_format.has_stencil_aspect() + }) + .then_some(wgpu::Operations { + load: keep_or_clear(color_load, 0), + store: if needs_backdrop { + wgpu::StoreOp::Store + } else { + wgpu::StoreOp::Discard + }, + }), + }, + ), + timestamp_writes: None, + occlusion_query_set: None, + multiview_mask: None, + }); + + // Forgetting the pass' lifetime means that we are no longer compile-time protected from + // runtime errors caused by accessing the parent encoder before the render pass is dropped. + // Since we don't pass it on to the renderer, we should be perfectly safe against this mistake here! + let mut render_pass = render_pass.forget_lifetime(); + let progress = renderer.render_from( + &mut render_pass, + clipped_primitives, + &screen_descriptor, + &mut cursor, + backdrop.as_ref(), + ); + // Ends the pass, so that the texture we drew into can be read. + drop(render_pass); + + if progress == RenderProgress::Done { + break; + } + + let Some(backdrop_texture) = backdrop_texture else { + // `needs_backdrop` said there were none, so `render_from` should never + // have stopped. + debug_assert!(false, "Bug in egui-wgpu: no backdrop texture was prepared"); + break; + }; + backdrop = renderer.capture_backdrop( + &render_state.device, + &render_state.queue, + &mut encoder, + clipped_primitives, + cursor, + target_texture, + backdrop_texture, + ); + // Keep what we have already drawn. + color_load = wgpu::LoadOp::Load; + } if capture && let Some(capture_state) = &mut self.screen_capture_state { - capture_buffer = Some(capture_state.copy_textures( - &render_state.device, - &output_frame, - &mut encoder, - )); + capture_buffer = + Some(capture_state.copy_to_buffer(&render_state.device, &mut encoder)); + } + if render_to_own_texture && let Some(capture_state) = &self.screen_capture_state { + capture_state.blit_to_surface(&output_frame, &mut encoder); } } diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index e5266aead..e1837169c 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -208,24 +208,64 @@ impl crate::TestRenderer for WgpuTestRenderer { let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - { - let mut pass = encoder - .begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Egui Render Pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &texture_view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - })], - ..Default::default() - }) - .forget_lifetime(); + // A paint callback may need to see what egui has already drawn, e.g. to blur the + // background behind a panel. Nothing can sample the texture it is drawing into, so + // that needs the render pass interrupted and the half-drawn frame copied aside. + let backdrop_texture = egui_wgpu::Renderer::needs_backdrop(&tessellated).then(|| { + egui_wgpu::BackdropTexture::new( + &self.render_state.device, + screen.size_in_pixels, + self.render_state.target_format, + ) + }); + let mut cursor = egui_wgpu::RenderCursor::default(); + let mut backdrop = None; + let mut load = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT); - renderer.render(&mut pass, &tessellated, &screen); + loop { + let progress = { + let mut pass = encoder + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Egui Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &texture_view, + resolve_target: None, + ops: wgpu::Operations { + load, + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + ..Default::default() + }) + .forget_lifetime(); + + renderer.render_from( + &mut pass, + &tessellated, + &screen, + &mut cursor, + backdrop.as_ref(), + ) + }; + + if progress == egui_wgpu::RenderProgress::Done { + break; + } + + let Some(backdrop_texture) = backdrop_texture.as_ref() else { + break; + }; + backdrop = renderer.capture_backdrop( + &self.render_state.device, + &self.render_state.queue, + &mut encoder, + &tessellated, + cursor, + &texture, + backdrop_texture, + ); + load = wgpu::LoadOp::Load; } self.render_state