1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -04:00

egui-wgpu: let paint callbacks read the backdrop

Adds `CallbackTrait::needs_backdrop`, so an effect can transform what egui has
already drawn rather than draw over it. Background blur behind a window or a
panel is the motivating case.

Nothing can sample the texture it is currently drawing into, so this needs the
render pass interrupted and the half-drawn frame copied aside. The renderer
does not own its pass, so the integration has to drive that: `render_from`
draws until it reaches a callback that wants a backdrop and stops, and
`capture_backdrop` copies the frame and lets the callback run its own passes
over it. `render` still works as before for integrations that do not care;
callbacks that asked for a backdrop just get a plain `paint`.

The surface texture cannot be copied from on every platform, which is why
`CaptureState` exists for screenshots. `Painter` now renders into that texture
whenever a backdrop is needed too, and blits it onto the surface afterwards, so
`copy_textures` is split into `copy_to_buffer` and `blit_to_surface`.

`egui_kittest`'s wgpu renderer drives the same loop, so backdrop effects can be
snapshot-tested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucas Meurer
2026-07-30 21:58:31 +02:00
parent b941904d4d
commit 32a8d1c378
4 changed files with 573 additions and 95 deletions

View File

@@ -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

View File

@@ -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::<Callback>()
&& 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<Backdrop<'a>> {
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::<Callback>()?;
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::<Callback>()
.is_some_and(|cbfn| cbfn.0.needs_backdrop()))
})
}
/// Should be called before [`Self::render`].

View File

@@ -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<T>(color_load: wgpu::LoadOp<wgpu::Color>, clear_value: T) -> wgpu::LoadOp<T> {
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<CaptureState>,
/// Somewhere to keep the half-drawn frame while a backdrop effect reads it.
backdrop_texture: Option<BackdropTexture>,
instance: wgpu::Instance,
render_state: Option<RenderState>,
@@ -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);
}
}

View File

@@ -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