mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
egui-wgpu: only copy the part of the frame a backdrop effect reads
`capture_backdrop` copied the whole target into the backdrop texture for every callback that asked for one, so a panel-sized blur on a large screen paid for the entire frame, once per blur. Ask the callback what it needs instead: `CallbackTrait::backdrop_rect` is given the rect the callback will be drawn into, its own rect already cut down by the clip rect, and returns the region it needs to read. Those are different questions — a blur reads its radius beyond every edge it draws — and keeping them apart is what lets the margin survive clipping, which inflating the callback's own rect could not do: that rect is also the viewport it draws into. `Backdrop::valid_in_pixels` reports what was captured. The texture stays full-size so a pixel is still where the effect expects it; only the region that was asked for holds this frame. The default answer is the rect the callback is drawn into, which is right for an effect that only reads the pixels it covers, so nothing has to change unless it reads further out. With nine blurred panels of 240x150 at radius 24 on a 1600x1200 canvas, this plus scissoring the blur passes in regui takes the frame rate from 146 to 1690 fps on WebGPU and 135 to 616 on WebGL2, measured in Chrome; the marginal cost of one blur drops about twentyfold. egui_kittest's snapshots of the same blurs come out pixel for pixel identical.
This commit is contained in:
@@ -431,6 +431,7 @@ impl WebPainter for WebPainterWgpu {
|
|||||||
&render_state.queue,
|
&render_state.queue,
|
||||||
&mut encoder,
|
&mut encoder,
|
||||||
clipped_primitives,
|
clipped_primitives,
|
||||||
|
&screen_descriptor,
|
||||||
cursor,
|
cursor,
|
||||||
target_texture,
|
target_texture,
|
||||||
backdrop_texture,
|
backdrop_texture,
|
||||||
|
|||||||
@@ -137,6 +137,27 @@ pub trait CallbackTrait: Send + Sync {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which part of the frame do you need to read, in points?
|
||||||
|
///
|
||||||
|
/// `drawn` is the rect you will be drawn into: your own rect, already cut down by the
|
||||||
|
/// clip rect, since nothing outside that reaches the screen. Return the region you need
|
||||||
|
/// to _read_, which is a different question — a blur reads its radius beyond every edge
|
||||||
|
/// it draws, and has to say so here or it will find stale pixels along that edge. The
|
||||||
|
/// answer is clamped to the screen, and is what [`Backdrop::valid_in_pixels`] reports.
|
||||||
|
///
|
||||||
|
/// The default of `drawn` suits an effect that only reads the pixels it covers, such as
|
||||||
|
/// a tint or a colour twist. Ask for what you need and no more: the copy costs in
|
||||||
|
/// proportion to the area, which is the whole point of asking. Sampling outside what you
|
||||||
|
/// asked for is not unsafe, it just shows you a stale frame.
|
||||||
|
///
|
||||||
|
/// Returning a region rather than a margin is what lets a one-sided effect, such as a
|
||||||
|
/// directional smear, pay for one side instead of four.
|
||||||
|
///
|
||||||
|
/// Only called when [`Self::needs_backdrop`] returns `true`.
|
||||||
|
fn backdrop_rect(&self, drawn: epaint::Rect) -> epaint::Rect {
|
||||||
|
drawn
|
||||||
|
}
|
||||||
|
|
||||||
/// Called between render passes, once the backdrop has been captured.
|
/// Called between render passes, once the backdrop has been captured.
|
||||||
///
|
///
|
||||||
/// You have the [`wgpu::CommandEncoder`], so this is where to run your own render
|
/// You have the [`wgpu::CommandEncoder`], so this is where to run your own render
|
||||||
@@ -198,6 +219,27 @@ impl Default for RenderCursor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The whole pixels a rect in points covers, clamped to a target `size_in_pixels` across.
|
||||||
|
///
|
||||||
|
/// Returns `[x, y, width, height]`, ready for a texture copy.
|
||||||
|
fn region_in_pixels(
|
||||||
|
rect: epaint::Rect,
|
||||||
|
pixels_per_point: f32,
|
||||||
|
size_in_pixels: [u32; 2],
|
||||||
|
) -> [u32; 4] {
|
||||||
|
let [width, height] = size_in_pixels.map(|size| size as f32);
|
||||||
|
let min_x = (rect.min.x * pixels_per_point).floor().clamp(0.0, width);
|
||||||
|
let min_y = (rect.min.y * pixels_per_point).floor().clamp(0.0, height);
|
||||||
|
let max_x = (rect.max.x * pixels_per_point).ceil().clamp(min_x, width);
|
||||||
|
let max_y = (rect.max.y * pixels_per_point).ceil().clamp(min_y, height);
|
||||||
|
[
|
||||||
|
min_x as u32,
|
||||||
|
min_y as u32,
|
||||||
|
(max_x - min_x) as u32,
|
||||||
|
(max_y - min_y) as u32,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether [`Renderer::render_from`] finished, or stopped to let you capture a backdrop.
|
/// Whether [`Renderer::render_from`] finished, or stopped to let you capture a backdrop.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum RenderProgress {
|
pub enum RenderProgress {
|
||||||
@@ -217,11 +259,24 @@ pub enum RenderProgress {
|
|||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct Backdrop<'a> {
|
pub struct Backdrop<'a> {
|
||||||
/// The captured image, ready to sample.
|
/// The captured image, ready to sample.
|
||||||
|
///
|
||||||
|
/// Only [`Self::valid_in_pixels`] holds this frame's image; the rest of the texture is
|
||||||
|
/// whatever was left there earlier. It is a full-size texture rather than a cut-out so
|
||||||
|
/// that a pixel is where you expect it: sample it at the position on screen, whichever
|
||||||
|
/// part was copied.
|
||||||
pub view: &'a wgpu::TextureView,
|
pub view: &'a wgpu::TextureView,
|
||||||
|
|
||||||
/// The size of [`Self::view`], in physical pixels.
|
/// The size of [`Self::view`], in physical pixels.
|
||||||
pub size_in_pixels: [u32; 2],
|
pub size_in_pixels: [u32; 2],
|
||||||
|
|
||||||
|
/// The part of [`Self::view`] that was captured this frame, in physical pixels, as
|
||||||
|
/// `[x, y, width, height]`.
|
||||||
|
///
|
||||||
|
/// This is what [`CallbackTrait::backdrop_rect`] asked for, clamped to the screen.
|
||||||
|
/// Copying the whole frame for a panel-sized effect is most of the cost of a backdrop,
|
||||||
|
/// so only what the effect said it would read is copied.
|
||||||
|
pub valid_in_pixels: [u32; 4],
|
||||||
|
|
||||||
/// The format of [`Self::view`].
|
/// The format of [`Self::view`].
|
||||||
pub format: wgpu::TextureFormat,
|
pub format: wgpu::TextureFormat,
|
||||||
}
|
}
|
||||||
@@ -239,7 +294,11 @@ impl BackdropTexture {
|
|||||||
/// Allocate a backdrop texture of the given size and format.
|
/// Allocate a backdrop texture of the given size and format.
|
||||||
///
|
///
|
||||||
/// The format must match the texture egui is being rendered into.
|
/// 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 {
|
pub fn new(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
size_in_pixels: [u32; 2],
|
||||||
|
format: wgpu::TextureFormat,
|
||||||
|
) -> Self {
|
||||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
label: Some("egui_backdrop"),
|
label: Some("egui_backdrop"),
|
||||||
size: wgpu::Extent3d {
|
size: wgpu::Extent3d {
|
||||||
@@ -259,7 +318,12 @@ impl BackdropTexture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Reallocate if the size or format no longer matches.
|
/// 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) {
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
size_in_pixels: [u32; 2],
|
||||||
|
format: wgpu::TextureFormat,
|
||||||
|
) {
|
||||||
let size = self.texture.size();
|
let size = self.texture.size();
|
||||||
if size.width != size_in_pixels[0].max(1)
|
if size.width != size_in_pixels[0].max(1)
|
||||||
|| size.height != size_in_pixels[1].max(1)
|
|| size.height != size_in_pixels[1].max(1)
|
||||||
@@ -642,7 +706,13 @@ impl Renderer {
|
|||||||
backdrops_supported: false,
|
backdrops_supported: false,
|
||||||
..RenderCursor::default()
|
..RenderCursor::default()
|
||||||
};
|
};
|
||||||
self.render_from(render_pass, paint_jobs, screen_descriptor, &mut cursor, None);
|
self.render_from(
|
||||||
|
render_pass,
|
||||||
|
paint_jobs,
|
||||||
|
screen_descriptor,
|
||||||
|
&mut cursor,
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw paint jobs, stopping when one of them needs a backdrop.
|
/// Draw paint jobs, stopping when one of them needs a backdrop.
|
||||||
@@ -861,12 +931,17 @@ impl Renderer {
|
|||||||
///
|
///
|
||||||
/// `backdrop` must already be the same size and format as `target`; call
|
/// `backdrop` must already be the same size and format as `target`; call
|
||||||
/// [`BackdropTexture::update`] before the loop starts.
|
/// [`BackdropTexture::update`] before the loop starts.
|
||||||
|
///
|
||||||
|
/// Only the part of `target` the callback said it would read is copied, which for a
|
||||||
|
/// panel-sized effect on a large screen is a small fraction of the frame. See
|
||||||
|
/// [`CallbackTrait::backdrop_rect`].
|
||||||
pub fn capture_backdrop<'a>(
|
pub fn capture_backdrop<'a>(
|
||||||
&self,
|
&self,
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
queue: &wgpu::Queue,
|
queue: &wgpu::Queue,
|
||||||
encoder: &mut wgpu::CommandEncoder,
|
encoder: &mut wgpu::CommandEncoder,
|
||||||
paint_jobs: &[epaint::ClippedPrimitive],
|
paint_jobs: &[epaint::ClippedPrimitive],
|
||||||
|
screen_descriptor: &ScreenDescriptor,
|
||||||
cursor: RenderCursor,
|
cursor: RenderCursor,
|
||||||
target: &wgpu::Texture,
|
target: &wgpu::Texture,
|
||||||
backdrop: &'a BackdropTexture,
|
backdrop: &'a BackdropTexture,
|
||||||
@@ -874,8 +949,8 @@ impl Renderer {
|
|||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
let Some(epaint::ClippedPrimitive {
|
let Some(epaint::ClippedPrimitive {
|
||||||
|
clip_rect,
|
||||||
primitive: Primitive::Callback(callback),
|
primitive: Primitive::Callback(callback),
|
||||||
..
|
|
||||||
}) = paint_jobs.get(cursor.job)
|
}) = paint_jobs.get(cursor.job)
|
||||||
else {
|
else {
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
@@ -900,15 +975,49 @@ impl Renderer {
|
|||||||
"The backdrop texture must be the same format as the texture egui is drawn into; call `BackdropTexture::update`"
|
"The backdrop texture must be the same format as the texture egui is drawn into; call `BackdropTexture::update`"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Nothing outside the clip rect reaches the screen, so that is all the callback can
|
||||||
|
// be drawn into. What it needs to read is a different question, so ask it.
|
||||||
|
let drawn = callback.rect.intersect(*clip_rect);
|
||||||
|
let wanted = cbfn.0.backdrop_rect(drawn);
|
||||||
|
let valid_in_pixels =
|
||||||
|
region_in_pixels(wanted, screen_descriptor.pixels_per_point, size_in_pixels);
|
||||||
|
let [x, y, width, height] = valid_in_pixels;
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
// The callback is off-screen or empty, so there is nothing to copy. Hand it a
|
||||||
|
// backdrop with an empty region rather than nothing at all, so it can still
|
||||||
|
// decide for itself whether to draw.
|
||||||
|
let backdrop = Backdrop {
|
||||||
|
view: &backdrop.view,
|
||||||
|
size_in_pixels,
|
||||||
|
valid_in_pixels,
|
||||||
|
format,
|
||||||
|
};
|
||||||
|
cbfn.0
|
||||||
|
.process_backdrop(device, queue, encoder, &self.callback_resources, &backdrop);
|
||||||
|
return Some(backdrop);
|
||||||
|
}
|
||||||
|
|
||||||
|
let origin = wgpu::Origin3d { x, y, z: 0 };
|
||||||
encoder.copy_texture_to_texture(
|
encoder.copy_texture_to_texture(
|
||||||
target.as_image_copy(),
|
wgpu::TexelCopyTextureInfo {
|
||||||
backdrop.texture.as_image_copy(),
|
origin,
|
||||||
size,
|
..target.as_image_copy()
|
||||||
|
},
|
||||||
|
wgpu::TexelCopyTextureInfo {
|
||||||
|
origin,
|
||||||
|
..backdrop.texture.as_image_copy()
|
||||||
|
},
|
||||||
|
wgpu::Extent3d {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let backdrop = Backdrop {
|
let backdrop = Backdrop {
|
||||||
view: &backdrop.view,
|
view: &backdrop.view,
|
||||||
size_in_pixels,
|
size_in_pixels,
|
||||||
|
valid_in_pixels,
|
||||||
format,
|
format,
|
||||||
};
|
};
|
||||||
cbfn.0
|
cbfn.0
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps
|
#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps
|
||||||
#![expect(unsafe_code)]
|
#![expect(unsafe_code)]
|
||||||
|
|
||||||
use crate::{RenderState, SurfaceConfig, SurfaceErrorAction, WgpuConfiguration, renderer};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
BackdropTexture, RenderCursor, RenderProgress, Renderer, RendererOptions,
|
BackdropTexture, RenderCursor, RenderProgress, Renderer, RendererOptions,
|
||||||
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
|
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
|
||||||
};
|
};
|
||||||
|
use crate::{RenderState, SurfaceConfig, SurfaceErrorAction, WgpuConfiguration, renderer};
|
||||||
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};
|
||||||
|
|
||||||
@@ -674,11 +674,8 @@ impl Painter {
|
|||||||
match &mut self.backdrop_texture {
|
match &mut self.backdrop_texture {
|
||||||
Some(backdrop) => backdrop.update(&render_state.device, size, format),
|
Some(backdrop) => backdrop.update(&render_state.device, size, format),
|
||||||
None => {
|
None => {
|
||||||
self.backdrop_texture = Some(BackdropTexture::new(
|
self.backdrop_texture =
|
||||||
&render_state.device,
|
Some(BackdropTexture::new(&render_state.device, size, format));
|
||||||
size,
|
|
||||||
format,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -785,6 +782,7 @@ impl Painter {
|
|||||||
&render_state.queue,
|
&render_state.queue,
|
||||||
&mut encoder,
|
&mut encoder,
|
||||||
clipped_primitives,
|
clipped_primitives,
|
||||||
|
&screen_descriptor,
|
||||||
cursor,
|
cursor,
|
||||||
target_texture,
|
target_texture,
|
||||||
backdrop_texture,
|
backdrop_texture,
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ impl crate::TestRenderer for WgpuTestRenderer {
|
|||||||
&self.render_state.queue,
|
&self.render_state.queue,
|
||||||
&mut encoder,
|
&mut encoder,
|
||||||
&tessellated,
|
&tessellated,
|
||||||
|
&screen,
|
||||||
cursor,
|
cursor,
|
||||||
&texture,
|
&texture,
|
||||||
backdrop_texture,
|
backdrop_texture,
|
||||||
|
|||||||
Reference in New Issue
Block a user