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:
@@ -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
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user