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

eframe: support backdrop effects on the web

The web painter called `Renderer::render`, which tells the renderer that no
backdrop can be captured, so a callback asking for one silently fell back to
`CallbackTrait::paint` and drew nothing. A blur behind a panel worked natively
and came out as plain glass in a browser.

Do on web what the winit painter already does: when a callback needs a
backdrop, render into our own texture rather than straight into the canvas
surface, which cannot be sampled, then run the
`render_from` / `capture_backdrop` loop and blit the result onto the surface.
The screen capture machinery is the same shape, so this reuses it, and frames
with no backdrop effect still go straight into the surface with no blit.

Checked in Chrome on both backends, WebGPU and WebGL2.
This commit is contained in:
Lucas Meurer
2026-08-02 16:37:53 +02:00
parent 211d19090a
commit b749228bcf

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use egui::{Event, UserData, ViewportId}; use egui::{Event, UserData, ViewportId};
use egui_wgpu::{ use egui_wgpu::{
RenderState, SurfaceErrorAction, BackdropTexture, RenderCursor, RenderProgress, RenderState, Renderer, SurfaceErrorAction,
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel}, capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
}; };
use wasm_bindgen::JsValue; use wasm_bindgen::JsValue;
@@ -20,6 +20,9 @@ pub(crate) struct WebPainterWgpu {
depth_stencil_format: Option<wgpu::TextureFormat>, depth_stencil_format: Option<wgpu::TextureFormat>,
depth_texture_view: Option<wgpu::TextureView>, depth_texture_view: Option<wgpu::TextureView>,
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>,
capture_tx: CaptureSender, capture_tx: CaptureSender,
capture_rx: CaptureReceiver, capture_rx: CaptureReceiver,
ctx: egui::Context, ctx: egui::Context,
@@ -27,6 +30,18 @@ pub(crate) struct WebPainterWgpu {
needs_recreate: bool, needs_recreate: bool,
} }
/// Keep what an earlier pass wrote, or clear if this is the first pass.
///
/// Backdrop effects split the frame into several passes; every pass after the first has to
/// load what the ones before it wrote, colour and depth alike.
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)
}
}
/// Owned web display handle that is `Send + Sync`. /// Owned web display handle that is `Send + Sync`.
/// ///
/// `DisplayHandle` from `raw-window-handle` is `!Send`/`!Sync` because the enum /// `DisplayHandle` from `raw-window-handle` is `!Send`/`!Sync` because the enum
@@ -139,6 +154,7 @@ impl WebPainterWgpu {
depth_texture_view: None, depth_texture_view: None,
on_surface_status: Arc::clone(&wgpu_options.on_surface_status) as _, on_surface_status: Arc::clone(&wgpu_options.on_surface_status) as _,
screen_capture_state: None, screen_capture_state: None,
backdrop_texture: None,
capture_tx, capture_tx,
capture_rx, capture_rx,
ctx, ctx,
@@ -282,85 +298,157 @@ impl WebPainter for WebPainterWgpu {
} }
}; };
// Backdrop effects have to read what egui has already drawn, and the surface
// texture cannot be read, 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;
{ {
let renderer = render_state.renderer.read(); let renderer = render_state.renderer.read();
let target_texture = if capture { 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);
}
&capture_state.texture let target_texture = self
} else { .screen_capture_state
&output_frame.texture .as_ref()
}; .filter(|_| render_to_own_texture)
.map_or(&output_frame.texture, |capture_state| {
&capture_state.texture
});
let target_view = let target_view =
target_texture.create_view(&wgpu::TextureViewDescriptor::default()); target_texture.create_view(&wgpu::TextureViewDescriptor::default());
let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { // Size the backdrop texture up front, so that the borrow of it can be held
color_attachments: &[Some(wgpu::RenderPassColorAttachment { // for the whole render loop below.
view: &target_view, if needs_backdrop {
resolve_target: None, let size = [target_texture.width(), target_texture.height()];
ops: wgpu::Operations { let format = target_texture.format();
load: wgpu::LoadOp::Clear(wgpu::Color { match &mut self.backdrop_texture {
r: clear_color[0] as f64, Some(backdrop) => backdrop.update(&render_state.device, size, format),
g: clear_color[1] as f64, None => {
b: clear_color[2] as f64, self.backdrop_texture =
a: clear_color[3] as f64, Some(BackdropTexture::new(&render_state.device, size, format));
}),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| {
wgpu::RenderPassDepthStencilAttachment {
view,
depth_ops: self
.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
.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,
}),
} }
}), }
label: Some("egui_render"), }
occlusion_query_set: None, let backdrop_texture = self.backdrop_texture.as_ref().filter(|_| needs_backdrop);
timestamp_writes: 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 loop {
// runtime errors caused by accessing the parent encoder before the render pass is dropped. let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
// Since we don't pass it on to the renderer, we should be perfectly safe against this mistake here! color_attachments: &[Some(wgpu::RenderPassColorAttachment {
renderer.render( view: &target_view,
&mut render_pass.forget_lifetime(), resolve_target: None,
clipped_primitives, ops: wgpu::Operations {
&screen_descriptor, load: color_load,
); store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| {
wgpu::RenderPassDepthStencilAttachment {
view,
depth_ops: self
.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
.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
},
}),
}
}),
label: Some("egui_render"),
occlusion_query_set: None,
timestamp_writes: 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;
}
} }
let capture_buffer = if capture let capture_buffer =
&& let Some(capture_state) = &mut self.screen_capture_state if capture && let Some(capture_state) = &mut self.screen_capture_state {
{ Some(capture_state.copy_to_buffer(&render_state.device, &mut encoder))
Some(capture_state.copy_textures(&render_state.device, &output_frame, &mut encoder)) } else {
} else { None
None };
}; if render_to_own_texture && let Some(capture_state) = &self.screen_capture_state {
capture_state.blit_to_surface(&output_frame, &mut encoder);
}
Some((output_frame, capture_buffer)) Some((output_frame, capture_buffer))
}; };