mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 06:40:06 -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:
@@ -117,12 +117,19 @@ impl CaptureState {
|
|||||||
output_frame: &wgpu::SurfaceTexture,
|
output_frame: &wgpu::SurfaceTexture,
|
||||||
encoder: &mut wgpu::CommandEncoder,
|
encoder: &mut wgpu::CommandEncoder,
|
||||||
) -> wgpu::Buffer {
|
) -> wgpu::Buffer {
|
||||||
debug_assert_eq!(
|
let buffer = self.copy_to_buffer(device, encoder);
|
||||||
self.texture.size(),
|
self.blit_to_surface(output_frame, encoder);
|
||||||
output_frame.texture.size(),
|
buffer
|
||||||
"Texture sizes must match, `CaptureState::update` was probably not called"
|
}
|
||||||
);
|
|
||||||
|
|
||||||
|
/// 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
|
// 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)
|
// 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.
|
// it might make sense to revisit this and implement a more efficient solution.
|
||||||
@@ -151,6 +158,25 @@ impl CaptureState {
|
|||||||
tex_extent,
|
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 {
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
label: Some("texture_copy"),
|
label: Some("texture_copy"),
|
||||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
@@ -171,8 +197,6 @@ impl CaptureState {
|
|||||||
pass.set_pipeline(&self.pipeline);
|
pass.set_pipeline(&self.pipeline);
|
||||||
pass.set_bind_group(0, &self.bind_group, &[]);
|
pass.set_bind_group(0, &self.bind_group, &[]);
|
||||||
pass.draw(0..3, 0..1);
|
pass.draw(0..3, 0..1);
|
||||||
|
|
||||||
buffer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles copying from the [`CaptureState`] texture to the surface texture and the cpu
|
/// Handles copying from the [`CaptureState`] texture to the surface texture and the cpu
|
||||||
|
|||||||
@@ -117,6 +117,162 @@ pub trait CallbackTrait: Send + Sync {
|
|||||||
render_pass: &mut wgpu::RenderPass<'static>,
|
render_pass: &mut wgpu::RenderPass<'static>,
|
||||||
callback_resources: &CallbackResources,
|
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.
|
/// Information about the screen used for rendering.
|
||||||
@@ -479,6 +635,62 @@ impl Renderer {
|
|||||||
paint_jobs: &[epaint::ClippedPrimitive],
|
paint_jobs: &[epaint::ClippedPrimitive],
|
||||||
screen_descriptor: &ScreenDescriptor,
|
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!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
let pixels_per_point = screen_descriptor.pixels_per_point;
|
let pixels_per_point = screen_descriptor.pixels_per_point;
|
||||||
@@ -488,14 +700,34 @@ impl Renderer {
|
|||||||
// run.
|
// run.
|
||||||
let mut needs_reset = true;
|
let mut needs_reset = true;
|
||||||
|
|
||||||
let mut index_buffer_slices = self.index_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();
|
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,
|
clip_rect,
|
||||||
primitive,
|
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 {
|
if needs_reset {
|
||||||
render_pass.set_viewport(
|
render_pass.set_viewport(
|
||||||
0.0,
|
0.0,
|
||||||
@@ -594,13 +826,108 @@ impl Renderer {
|
|||||||
1.0,
|
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]);
|
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`].
|
/// Should be called before [`Self::render`].
|
||||||
|
|||||||
@@ -5,12 +5,22 @@
|
|||||||
|
|
||||||
use crate::{RenderState, SurfaceConfig, SurfaceErrorAction, WgpuConfiguration, renderer};
|
use crate::{RenderState, SurfaceConfig, SurfaceErrorAction, WgpuConfiguration, renderer};
|
||||||
use crate::{
|
use crate::{
|
||||||
RendererOptions,
|
BackdropTexture, RenderCursor, RenderProgress, Renderer, RendererOptions,
|
||||||
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
|
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
|
||||||
};
|
};
|
||||||
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
|
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
|
||||||
use std::{num::NonZeroU32, sync::Arc};
|
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 {
|
struct SurfaceState {
|
||||||
surface: wgpu::Surface<'static>,
|
surface: wgpu::Surface<'static>,
|
||||||
alpha_mode: wgpu::CompositeAlphaMode,
|
alpha_mode: wgpu::CompositeAlphaMode,
|
||||||
@@ -33,6 +43,9 @@ pub struct Painter {
|
|||||||
support_transparent_backbuffer: bool,
|
support_transparent_backbuffer: bool,
|
||||||
screen_capture_state: Option<CaptureState>,
|
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,
|
instance: wgpu::Instance,
|
||||||
render_state: Option<RenderState>,
|
render_state: Option<RenderState>,
|
||||||
|
|
||||||
@@ -72,6 +85,7 @@ impl Painter {
|
|||||||
options,
|
options,
|
||||||
support_transparent_backbuffer,
|
support_transparent_backbuffer,
|
||||||
screen_capture_state: None,
|
screen_capture_state: None,
|
||||||
|
backdrop_texture: None,
|
||||||
|
|
||||||
instance,
|
instance,
|
||||||
render_state: None,
|
render_state: None,
|
||||||
@@ -630,18 +644,46 @@ impl Painter {
|
|||||||
{
|
{
|
||||||
let renderer = render_state.renderer.read();
|
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(|| {
|
let capture_state = self.screen_capture_state.get_or_insert_with(|| {
|
||||||
CaptureState::new(&render_state.device, &output_frame.texture)
|
CaptureState::new(&render_state.device, &output_frame.texture)
|
||||||
});
|
});
|
||||||
capture_state.update(&render_state.device, &output_frame.texture);
|
capture_state.update(&render_state.device, &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
|
&capture_state.texture
|
||||||
} else {
|
});
|
||||||
&output_frame.texture
|
|
||||||
};
|
|
||||||
let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
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)
|
let (view, resolve_target) = (self.options.msaa_samples > 1)
|
||||||
.then_some(self.msaa_texture_view.get(&viewport_id))
|
.then_some(self.msaa_texture_view.get(&viewport_id))
|
||||||
.flatten()
|
.flatten()
|
||||||
@@ -649,24 +691,32 @@ impl Painter {
|
|||||||
(texture_view, Some(&target_view))
|
(texture_view, Some(&target_view))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
});
|
||||||
|
|
||||||
|
loop {
|
||||||
let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
label: Some("egui_render"),
|
label: Some("egui_render"),
|
||||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
view,
|
view,
|
||||||
resolve_target,
|
resolve_target,
|
||||||
ops: wgpu::Operations {
|
ops: wgpu::Operations {
|
||||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
load: color_load,
|
||||||
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,
|
store: wgpu::StoreOp::Store,
|
||||||
},
|
},
|
||||||
depth_slice: None,
|
depth_slice: None,
|
||||||
})],
|
})],
|
||||||
depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| {
|
depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(
|
||||||
wgpu::RenderPassDepthStencilAttachment {
|
|view| wgpu::RenderPassDepthStencilAttachment {
|
||||||
view,
|
view,
|
||||||
depth_ops: self
|
depth_ops: self
|
||||||
.options
|
.options
|
||||||
@@ -675,10 +725,15 @@ impl Painter {
|
|||||||
depth_stencil_format.has_depth_aspect()
|
depth_stencil_format.has_depth_aspect()
|
||||||
})
|
})
|
||||||
.then_some(wgpu::Operations {
|
.then_some(wgpu::Operations {
|
||||||
load: wgpu::LoadOp::Clear(1.0),
|
load: keep_or_clear(color_load, 1.0),
|
||||||
// It is very unlikely that the depth buffer is needed after egui finished rendering
|
// 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)
|
// so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon).
|
||||||
store: wgpu::StoreOp::Discard,
|
// 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
|
stencil_ops: self
|
||||||
.options
|
.options
|
||||||
@@ -687,11 +742,15 @@ impl Painter {
|
|||||||
depth_stencil_format.has_stencil_aspect()
|
depth_stencil_format.has_stencil_aspect()
|
||||||
})
|
})
|
||||||
.then_some(wgpu::Operations {
|
.then_some(wgpu::Operations {
|
||||||
load: wgpu::LoadOp::Clear(0),
|
load: keep_or_clear(color_load, 0),
|
||||||
store: wgpu::StoreOp::Discard,
|
store: if needs_backdrop {
|
||||||
}),
|
wgpu::StoreOp::Store
|
||||||
}
|
} else {
|
||||||
|
wgpu::StoreOp::Discard
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
timestamp_writes: None,
|
timestamp_writes: None,
|
||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
multiview_mask: None,
|
multiview_mask: None,
|
||||||
@@ -700,18 +759,46 @@ impl Painter {
|
|||||||
// Forgetting the pass' lifetime means that we are no longer compile-time protected from
|
// 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.
|
// 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!
|
// Since we don't pass it on to the renderer, we should be perfectly safe against this mistake here!
|
||||||
renderer.render(
|
let mut render_pass = render_pass.forget_lifetime();
|
||||||
&mut render_pass.forget_lifetime(),
|
let progress = renderer.render_from(
|
||||||
|
&mut render_pass,
|
||||||
clipped_primitives,
|
clipped_primitives,
|
||||||
&screen_descriptor,
|
&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 {
|
if capture && let Some(capture_state) = &mut self.screen_capture_state {
|
||||||
capture_buffer = Some(capture_state.copy_textures(
|
capture_buffer =
|
||||||
&render_state.device,
|
Some(capture_state.copy_to_buffer(&render_state.device, &mut encoder));
|
||||||
&output_frame,
|
}
|
||||||
&mut encoder,
|
if render_to_own_texture && let Some(capture_state) = &self.screen_capture_state {
|
||||||
));
|
capture_state.blit_to_surface(&output_frame, &mut encoder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -208,7 +208,22 @@ impl crate::TestRenderer for WgpuTestRenderer {
|
|||||||
|
|
||||||
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
{
|
// 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);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let progress = {
|
||||||
let mut pass = encoder
|
let mut pass = encoder
|
||||||
.begin_render_pass(&wgpu::RenderPassDescriptor {
|
.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
label: Some("Egui Render Pass"),
|
label: Some("Egui Render Pass"),
|
||||||
@@ -216,7 +231,7 @@ impl crate::TestRenderer for WgpuTestRenderer {
|
|||||||
view: &texture_view,
|
view: &texture_view,
|
||||||
resolve_target: None,
|
resolve_target: None,
|
||||||
ops: wgpu::Operations {
|
ops: wgpu::Operations {
|
||||||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
load,
|
||||||
store: wgpu::StoreOp::Store,
|
store: wgpu::StoreOp::Store,
|
||||||
},
|
},
|
||||||
depth_slice: None,
|
depth_slice: None,
|
||||||
@@ -225,7 +240,32 @@ impl crate::TestRenderer for WgpuTestRenderer {
|
|||||||
})
|
})
|
||||||
.forget_lifetime();
|
.forget_lifetime();
|
||||||
|
|
||||||
renderer.render(&mut pass, &tessellated, &screen);
|
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
|
self.render_state
|
||||||
|
|||||||
Reference in New Issue
Block a user