Browse Source

Re-implement PaintCallbacks With Support for WGPU (#1684)

* Re-implement PaintCallbacks With Support for WGPU

This makes breaking changes to the PaintCallback system, but makes it
flexible enough to support both the WGPU and glow backends with custom
rendering.

Also adds a WGPU equivalent to the glow demo for custom painting.
pull/1693/head
Zicklag 2 years ago
committed by GitHub
parent
commit
1d9524cc59
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      CHANGELOG.md
  2. 12
      Cargo.lock
  3. 10
      eframe/src/epi.rs
  4. 3
      eframe/src/lib.rs
  5. 3
      eframe/src/native/epi_integration.rs
  6. 8
      eframe/src/native/run.rs
  7. 4
      eframe/src/web/backend.rs
  8. 1
      egui-wgpu/Cargo.toml
  9. 3
      egui-wgpu/src/lib.rs
  10. 253
      egui-wgpu/src/renderer.rs
  11. 63
      egui-wgpu/src/winit.rs
  12. 4
      egui_demo_app/Cargo.toml
  13. 18
      egui_demo_app/src/apps/custom3d_glow.rs
  14. 177
      egui_demo_app/src/apps/custom3d_wgpu.rs
  15. 39
      egui_demo_app/src/apps/custom3d_wgpu_shader.wgsl
  16. 14
      egui_demo_app/src/apps/mod.rs
  17. 24
      egui_demo_app/src/wrap_app.rs
  18. 2
      egui_glow/src/lib.rs
  19. 31
      egui_glow/src/painter.rs
  20. 26
      epaint/src/shape.rs
  21. 10
      examples/custom_3d_glow/src/main.rs
  22. 14
      examples/custom_3d_three-d/src/main.rs

4
CHANGELOG.md

@ -12,6 +12,10 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
* Added opt-in feature `deadlock_detection` to detect double-lock of mutexes on the same thread ([#1619](https://github.com/emilk/egui/pull/1619)).
* Added `InputState::stable_dt`: a more stable estimate for the delta-time in reactive mode ([#1625](https://github.com/emilk/egui/pull/1625)).
* You can now specify a texture filter for your textures ([#1636](https://github.com/emilk/egui/pull/1636)).
* Added support for using `PaintCallback` shapes with the WGPU backend ([#1684](https://github.com/emilk/egui/pull/1684))
### Changed
* `PaintCallback` shapes now require the whole callback to be put in an `Arc<dyn Any>` with the value being a backend-specific callback type. ([#1684](https://github.com/emilk/egui/pull/1684))
### Fixed 🐛
* Fixed `ImageButton`'s changing background padding on hover ([#1595](https://github.com/emilk/egui/pull/1595)).

12
Cargo.lock

@ -1168,6 +1168,7 @@ dependencies = [
"egui",
"pollster",
"tracing",
"type-map",
"wgpu",
"winit",
]
@ -1192,6 +1193,7 @@ dependencies = [
name = "egui_demo_app"
version = "0.18.0"
dependencies = [
"bytemuck",
"chrono",
"console_error_panic_hook",
"eframe",
@ -1201,6 +1203,7 @@ dependencies = [
"ehttp",
"image",
"poll-promise",
"pollster",
"serde",
"tracing-subscriber",
"tracing-wasm",
@ -3720,6 +3723,15 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "type-map"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f"
dependencies = [
"rustc-hash",
]
[[package]]
name = "unicode-bidi"
version = "0.3.8"

10
eframe/src/epi.rs

@ -29,6 +29,11 @@ pub struct CreationContext<'s> {
/// you might want to use later from a [`egui::PaintCallback`].
#[cfg(feature = "glow")]
pub gl: Option<std::sync::Arc<glow::Context>>,
/// Can be used to manage GPU resources for custom rendering with WGPU using
/// [`egui::PaintCallback`]s.
#[cfg(feature = "wgpu")]
pub render_state: Option<egui_wgpu::RenderState>,
}
// ----------------------------------------------------------------------------
@ -335,6 +340,11 @@ pub struct Frame {
#[cfg(feature = "glow")]
#[doc(hidden)]
pub gl: Option<std::sync::Arc<glow::Context>>,
/// Can be used to manage GPU resources for custom rendering with WGPU using
/// [`egui::PaintCallback`]s.
#[cfg(feature = "wgpu")]
pub render_state: Option<egui_wgpu::RenderState>,
}
impl Frame {

3
eframe/src/lib.rs

@ -61,6 +61,9 @@ pub use {egui, egui::emath, egui::epaint};
#[cfg(feature = "glow")]
pub use {egui_glow, glow};
#[cfg(feature = "wgpu")]
pub use {egui_wgpu, wgpu};
mod epi;
// Re-export everything in `epi` so `eframe` users don't have to care about what `epi` is:

3
eframe/src/native/epi_integration.rs

@ -188,6 +188,7 @@ impl EpiIntegration {
window: &winit::window::Window,
storage: Option<Box<dyn epi::Storage>>,
#[cfg(feature = "glow")] gl: Option<std::sync::Arc<glow::Context>>,
#[cfg(feature = "wgpu")] render_state: Option<egui_wgpu::RenderState>,
) -> Self {
let egui_ctx = egui::Context::default();
@ -207,6 +208,8 @@ impl EpiIntegration {
storage,
#[cfg(feature = "glow")]
gl,
#[cfg(feature = "wgpu")]
render_state,
};
if prefer_dark_mode == Some(true) {

8
eframe/src/native/run.rs

@ -62,6 +62,8 @@ pub fn run_glow(
gl_window.window(),
storage,
Some(gl.clone()),
#[cfg(feature = "wgpu")]
None,
);
{
@ -76,6 +78,8 @@ pub fn run_glow(
integration_info: integration.frame.info(),
storage: integration.frame.storage(),
gl: Some(gl.clone()),
#[cfg(feature = "wgpu")]
render_state: None,
});
if app.warm_up_enabled() {
@ -230,6 +234,8 @@ pub fn run_wgpu(
painter
};
let render_state = painter.get_render_state().expect("Uninitialized");
let mut integration = epi_integration::EpiIntegration::new(
&event_loop,
painter.max_texture_side().unwrap_or(2048),
@ -237,6 +243,7 @@ pub fn run_wgpu(
storage,
#[cfg(feature = "glow")]
None,
Some(render_state.clone()),
);
{
@ -252,6 +259,7 @@ pub fn run_wgpu(
storage: integration.frame.storage(),
#[cfg(feature = "glow")]
gl: None,
render_state: Some(render_state),
});
if app.warm_up_enabled() {

4
eframe/src/web/backend.rs

@ -170,6 +170,8 @@ impl AppRunner {
storage: Some(&storage),
#[cfg(feature = "glow")]
gl: Some(painter.painter.gl().clone()),
#[cfg(feature = "wgpu")]
render_state: None,
});
let frame = epi::Frame {
@ -178,6 +180,8 @@ impl AppRunner {
storage: Some(Box::new(storage)),
#[cfg(feature = "glow")]
gl: Some(painter.gl().clone()),
#[cfg(feature = "wgpu")]
render_state: None,
};
let needs_repaint: std::sync::Arc<NeedRepaint> = Default::default();

1
egui-wgpu/Cargo.toml

@ -37,6 +37,7 @@ egui = { version = "0.18.1", path = "../egui", default-features = false, feature
bytemuck = "1.7"
tracing = "0.1"
type-map = "0.5.0"
wgpu = { version = "0.12", features = ["webgl"] }
# Optional:

3
egui-wgpu/src/lib.rs

@ -6,7 +6,10 @@ pub use wgpu;
/// Low-level painting of [`egui`] on [`wgpu`].
pub mod renderer;
pub use renderer::CallbackFn;
/// Module for painting [`egui`] with [`wgpu`] on [`winit`].
#[cfg(feature = "winit")]
pub mod winit;
#[cfg(feature = "winit")]
pub use crate::winit::RenderState;

253
egui-wgpu/src/renderer.rs

@ -2,10 +2,79 @@
use std::{borrow::Cow, collections::HashMap, num::NonZeroU32};
use egui::epaint::Primitive;
use egui::{epaint::Primitive, PaintCallbackInfo};
use type_map::TypeMap;
use wgpu;
use wgpu::util::DeviceExt as _;
/// A callback function that can be used to compose an [`egui::PaintCallback`] for custom WGPU
/// rendering.
///
/// The callback is composed of two functions: `prepare` and `paint`.
///
/// `prepare` is called every frame before `paint`, and can use the passed-in [`wgpu::Device`] and
/// [`wgpu::Buffer`] to allocate or modify GPU resources such as buffers.
///
/// `paint` is called after `prepare` and is given access to the the [`wgpu::RenderPass`] so that it
/// can issue draw commands.
///
/// The final argument of both the `prepare` and `paint` callbacks is a the
/// [`paint_callback_resources`][crate::renderer::RenderPass::paint_callback_resources].
/// `paint_callback_resources` has the same lifetime as the Egui render pass, so it can be used to
/// store buffers, pipelines, and other information that needs to be accessed during the render
/// pass.
///
/// # Example
///
/// See the [custom3d_wgpu] demo source for a detailed usage example.
///
/// [custom3d_wgpu]:
/// https://github.com/emilk/egui/blob/master/egui_demo_app/src/apps/custom3d_wgpu.rs
pub struct CallbackFn {
prepare: Box<PrepareCallback>,
paint: Box<PaintCallback>,
}
type PrepareCallback = dyn Fn(&wgpu::Device, &wgpu::Queue, &mut TypeMap) + Sync + Send;
type PaintCallback =
dyn for<'a, 'b> Fn(PaintCallbackInfo, &'a mut wgpu::RenderPass<'b>, &'b TypeMap) + Sync + Send;
impl Default for CallbackFn {
fn default() -> Self {
CallbackFn {
prepare: Box::new(|_, _, _| ()),
paint: Box::new(|_, _, _| ()),
}
}
}
impl CallbackFn {
pub fn new() -> Self {
Self::default()
}
/// Set the prepare callback
pub fn prepare<F>(mut self, prepare: F) -> Self
where
F: Fn(&wgpu::Device, &wgpu::Queue, &mut TypeMap) + Sync + Send + 'static,
{
self.prepare = Box::new(prepare) as _;
self
}
/// Set the paint callback
pub fn paint<F>(mut self, paint: F) -> Self
where
F: for<'a, 'b> Fn(PaintCallbackInfo, &'a mut wgpu::RenderPass<'b>, &'b TypeMap)
+ Sync
+ Send
+ 'static,
{
self.paint = Box::new(paint) as _;
self
}
}
/// Enum for selecting the right buffer type.
#[derive(Debug)]
enum BufferType {
@ -61,6 +130,9 @@ pub struct RenderPass {
/// sampler.
textures: HashMap<egui::TextureId, (Option<wgpu::Texture>, wgpu::BindGroup)>,
next_user_texture_id: u64,
/// Storage for use by [`egui::PaintCallback`]'s that need to store resources such as render
/// pipelines that must have the lifetime of the renderpass.
pub paint_callback_resources: type_map::TypeMap,
}
impl RenderPass {
@ -214,6 +286,7 @@ impl RenderPass {
texture_bind_group_layout,
textures: HashMap::new(),
next_user_texture_id: 0,
paint_callback_resources: TypeMap::default(),
}
}
@ -258,13 +331,13 @@ impl RenderPass {
paint_jobs: &[egui::epaint::ClippedPrimitive],
screen_descriptor: &ScreenDescriptor,
) {
rpass.set_pipeline(&self.render_pipeline);
rpass.set_bind_group(0, &self.uniform_bind_group, &[]);
let pixels_per_point = screen_descriptor.pixels_per_point;
let size_in_pixels = screen_descriptor.size_in_pixels;
// Whether or not we need to reset the renderpass state because a paint callback has just
// run.
let mut needs_reset = true;
for (
(
egui::ClippedPrimitive {
@ -279,41 +352,34 @@ impl RenderPass {
.zip(&self.vertex_buffers)
.zip(&self.index_buffers)
{
// Transform clip rect to physical pixels.
let clip_min_x = pixels_per_point * clip_rect.min.x;
let clip_min_y = pixels_per_point * clip_rect.min.y;
let clip_max_x = pixels_per_point * clip_rect.max.x;
let clip_max_y = pixels_per_point * clip_rect.max.y;
// Make sure clip rect can fit within an `u32`.
let clip_min_x = clip_min_x.clamp(0.0, size_in_pixels[0] as f32);
let clip_min_y = clip_min_y.clamp(0.0, size_in_pixels[1] as f32);
let clip_max_x = clip_max_x.clamp(clip_min_x, size_in_pixels[0] as f32);
let clip_max_y = clip_max_y.clamp(clip_min_y, size_in_pixels[1] as f32);
let clip_min_x = clip_min_x.round() as u32;
let clip_min_y = clip_min_y.round() as u32;
let clip_max_x = clip_max_x.round() as u32;
let clip_max_y = clip_max_y.round() as u32;
let width = (clip_max_x - clip_min_x).max(1);
let height = (clip_max_y - clip_min_y).max(1);
{
// Clip scissor rectangle to target size.
let x = clip_min_x.min(size_in_pixels[0]);
let y = clip_min_y.min(size_in_pixels[1]);
let width = width.min(size_in_pixels[0] - x);
let height = height.min(size_in_pixels[1] - y);
// Skip rendering with zero-sized clip areas.
if width == 0 || height == 0 {
continue;
}
if needs_reset {
rpass.set_viewport(
0.0,
0.0,
size_in_pixels[0] as f32,
size_in_pixels[1] as f32,
0.0,
1.0,
);
rpass.set_pipeline(&self.render_pipeline);
rpass.set_bind_group(0, &self.uniform_bind_group, &[]);
needs_reset = false;
}
let PixelRect {
x,
y,
width,
height,
} = calculate_pixel_rect(clip_rect, pixels_per_point, size_in_pixels);
rpass.set_scissor_rect(x, y, width, height);
// Skip rendering with zero-sized clip areas.
if width == 0 || height == 0 {
continue;
}
rpass.set_scissor_rect(x, y, width, height);
match primitive {
Primitive::Mesh(mesh) => {
if let Some((_texture, bind_group)) = self.textures.get(&mesh.texture_id) {
@ -328,8 +394,57 @@ impl RenderPass {
tracing::warn!("Missing texture: {:?}", mesh.texture_id);
}
}
Primitive::Callback(_) => {
// already warned about earlier
Primitive::Callback(callback) => {
let cbfn = if let Some(c) = callback.callback.downcast_ref::<CallbackFn>() {
c
} else {
// We already warned in the `prepare` callback
continue;
};
if callback.rect.is_positive() {
needs_reset = true;
// Set the viewport rect
let PixelRect {
x,
y,
width,
height,
} = calculate_pixel_rect(&callback.rect, pixels_per_point, size_in_pixels);
rpass.set_viewport(
x as f32,
y as f32,
width as f32,
height as f32,
0.0,
1.0,
);
// Set the scissor rect
let PixelRect {
x,
y,
width,
height,
} = calculate_pixel_rect(clip_rect, pixels_per_point, size_in_pixels);
// Skip rendering with zero-sized clip areas.
if width == 0 || height == 0 {
continue;
}
rpass.set_scissor_rect(x, y, width, height);
(cbfn.paint)(
PaintCallbackInfo {
viewport: callback.rect,
clip_rect: *clip_rect,
pixels_per_point,
screen_size_px: size_in_pixels,
},
rpass,
&self.paint_callback_resources,
);
}
}
}
}
@ -448,7 +563,6 @@ impl RenderPass {
};
}
/// Should be called before `execute()`.
pub fn free_texture(&mut self, id: &egui::TextureId) {
self.textures.remove(id);
}
@ -587,8 +701,15 @@ impl RenderPass {
});
}
}
Primitive::Callback(_) => {
tracing::warn!("Painting callbacks not supported by egui-wgpu (yet)");
Primitive::Callback(callback) => {
let cbfn = if let Some(c) = callback.callback.downcast_ref::<CallbackFn>() {
c
} else {
tracing::warn!("Unknown paint callback: expected `egui_gpu::CallbackFn`");
continue;
};
(cbfn.prepare)(device, queue, &mut self.paint_callback_resources);
}
}
}
@ -633,3 +754,51 @@ impl RenderPass {
}
}
}
/// A Rect in physical pixel space, used for setting viewport and cliipping rectangles.
struct PixelRect {
x: u32,
y: u32,
width: u32,
height: u32,
}
/// Convert the Egui clip rect to a physical pixel rect we can use for the GPU viewport/scissor
fn calculate_pixel_rect(
clip_rect: &egui::Rect,
pixels_per_point: f32,
target_size: [u32; 2],
) -> PixelRect {
// Transform clip rect to physical pixels.
let clip_min_x = pixels_per_point * clip_rect.min.x;
let clip_min_y = pixels_per_point * clip_rect.min.y;
let clip_max_x = pixels_per_point * clip_rect.max.x;
let clip_max_y = pixels_per_point * clip_rect.max.y;
// Make sure clip rect can fit within an `u32`.
let clip_min_x = clip_min_x.clamp(0.0, target_size[0] as f32);
let clip_min_y = clip_min_y.clamp(0.0, target_size[1] as f32);
let clip_max_x = clip_max_x.clamp(clip_min_x, target_size[0] as f32);
let clip_max_y = clip_max_y.clamp(clip_min_y, target_size[1] as f32);
let clip_min_x = clip_min_x.round() as u32;
let clip_min_y = clip_min_y.round() as u32;
let clip_max_x = clip_max_x.round() as u32;
let clip_max_y = clip_max_y.round() as u32;
let width = (clip_max_x - clip_min_x).max(1);
let height = (clip_max_y - clip_min_y).max(1);
// Clip scissor rectangle to target size.
let x = clip_min_x.min(target_size[0]);
let y = clip_min_y.min(target_size[1]);
let width = width.min(target_size[0] - x);
let height = height.min(target_size[1] - y);
PixelRect {
x,
y,
width,
height,
}
}

63
egui-wgpu/src/winit.rs

@ -1,13 +1,19 @@
use std::sync::Arc;
use egui::mutex::RwLock;
use tracing::error;
use wgpu::{Adapter, Instance, Surface, TextureFormat};
use crate::renderer;
struct RenderState {
device: wgpu::Device,
queue: wgpu::Queue,
target_format: TextureFormat,
egui_rpass: renderer::RenderPass,
/// Access to the render state for egui, which can be useful in combination with
/// [`egui::PaintCallback`]s for custom rendering using WGPU.
#[derive(Clone)]
pub struct RenderState {
pub device: Arc<wgpu::Device>,
pub queue: Arc<wgpu::Queue>,
pub target_format: TextureFormat,
pub egui_rpass: Arc<RwLock<renderer::RenderPass>>,
}
struct SurfaceState {
@ -66,6 +72,13 @@ impl<'a> Painter<'a> {
}
}
/// Get the [`RenderState`].
///
/// Will return [`None`] if the render state has not been initialized yet.
pub fn get_render_state(&self) -> Option<RenderState> {
self.render_state.as_ref().cloned()
}
async fn init_render_state(
&self,
adapter: &Adapter,
@ -74,13 +87,13 @@ impl<'a> Painter<'a> {
let (device, queue) =
pollster::block_on(adapter.request_device(&self.device_descriptor, None)).unwrap();
let egui_rpass = renderer::RenderPass::new(&device, target_format, self.msaa_samples);
let rpass = renderer::RenderPass::new(&device, target_format, self.msaa_samples);
RenderState {
device,
queue,
device: Arc::new(device),
queue: Arc::new(queue),
target_format,
egui_rpass,
egui_rpass: Arc::new(RwLock::new(rpass)),
}
}
@ -246,27 +259,22 @@ impl<'a> Painter<'a> {
pixels_per_point,
};
for (id, image_delta) in &textures_delta.set {
render_state.egui_rpass.update_texture(
{
let mut rpass = render_state.egui_rpass.write();
for (id, image_delta) in &textures_delta.set {
rpass.update_texture(&render_state.device, &render_state.queue, *id, image_delta);
}
rpass.update_buffers(
&render_state.device,
&render_state.queue,
*id,
image_delta,
clipped_primitives,
&screen_descriptor,
);
}
for id in &textures_delta.free {
render_state.egui_rpass.free_texture(id);
}
render_state.egui_rpass.update_buffers(
&render_state.device,
&render_state.queue,
clipped_primitives,
&screen_descriptor,
);
// Record all render passes.
render_state.egui_rpass.execute(
render_state.egui_rpass.read().execute(
&mut encoder,
&output_view,
clipped_primitives,
@ -279,6 +287,13 @@ impl<'a> Painter<'a> {
}),
);
{
let mut rpass = render_state.egui_rpass.write();
for id in &textures_delta.free {
rpass.free_texture(id);
}
}
// Submit the commands.
render_state.queue.submit(std::iter::once(encoder.finish()));

4
egui_demo_app/Cargo.toml

@ -34,7 +34,7 @@ serde = [
syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]
glow = ["eframe/glow"]
wgpu = ["eframe/wgpu"]
wgpu = ["eframe/wgpu", "bytemuck", "pollster"]
[dependencies]
@ -45,6 +45,7 @@ egui_demo_lib = { version = "0.18.0", path = "../egui_demo_lib", features = ["ch
# Optional dependencies:
bytemuck = { version = "1.9.1", optional = true }
egui_extras = { version = "0.18.0", optional = true, path = "../egui_extras" }
# feature "http":
@ -54,6 +55,7 @@ image = { version = "0.24", optional = true, default-features = false, features
"png",
] }
poll-promise = { version = "0.1", optional = true, default-features = false }
pollster = { version = "0.2.5", optional = true }
# feature "persistence":
serde = { version = "1", optional = true, features = ["derive"] }

18
egui_demo_app/src/apps/custom3d.rs → egui_demo_app/src/apps/custom3d_glow.rs

@ -11,9 +11,11 @@ pub struct Custom3d {
}
impl Custom3d {
pub fn new(gl: &glow::Context) -> Self {
pub fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Self {
Self {
rotating_triangle: Arc::new(Mutex::new(RotatingTriangle::new(gl))),
rotating_triangle: Arc::new(Mutex::new(RotatingTriangle::new(
cc.gl.as_ref().expect("GL Enabled"),
))),
angle: 0.0,
}
}
@ -58,15 +60,13 @@ impl Custom3d {
let angle = self.angle;
let rotating_triangle = self.rotating_triangle.clone();
let cb = egui_glow::CallbackFn::new(move |_info, painter| {
rotating_triangle.lock().paint(painter.gl(), angle);
});
let callback = egui::PaintCallback {
rect,
callback: std::sync::Arc::new(move |_info, render_ctx| {
if let Some(painter) = render_ctx.downcast_ref::<egui_glow::Painter>() {
rotating_triangle.lock().paint(painter.gl(), angle);
} else {
eprintln!("Can't do custom painting because we are not using a glow context");
}
}),
callback: Arc::new(cb),
};
ui.painter().add(callback);
}

177
egui_demo_app/src/apps/custom3d_wgpu.rs

@ -0,0 +1,177 @@
use std::sync::Arc;
use eframe::{
egui_wgpu::{self, wgpu},
wgpu::util::DeviceExt,
};
pub struct Custom3d {
angle: f32,
}
impl Custom3d {
pub fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Self {
// Get the WGPU render state from the eframe creation context. This can also be retrieved
// from `eframe::Frame` when you don't have a `CreationContext` available.
let render_state = cc.render_state.as_ref().expect("WGPU enabled");
let device = &render_state.device;
let shader = device.create_shader_module(&wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(include_str!("./custom3d_wgpu_shader.wgsl").into()),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[render_state.target_format.into()],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
});
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
contents: bytemuck::cast_slice(&[0.0]),
usage: wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::MAP_WRITE
| wgpu::BufferUsages::UNIFORM,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
// Because the graphics pipeline must have the same lifetime as the egui render pass,
// instead of storing the pipeline in our `Custom3D` struct, we insert it into the
// `paint_callback_resources` type map, which is stored alongside the render pass.
render_state
.egui_rpass
.write()
.paint_callback_resources
.insert(TriangleRenderResources {
pipeline,
bind_group,
uniform_buffer,
});
Self { angle: 0.0 }
}
}
impl eframe::App for Custom3d {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("The triangle is being painted using ");
ui.hyperlink_to("WGPU", "https://wgpu.rs");
ui.label(" (Portable Rust graphics API awesomeness)");
});
ui.label(
"It's not a very impressive demo, but it shows you can embed 3D inside of egui.",
);
egui::Frame::canvas(ui.style()).show(ui, |ui| {
self.custom_painting(ui);
});
ui.label("Drag to rotate!");
ui.add(egui_demo_lib::egui_github_link_file!());
});
}
}
impl Custom3d {
fn custom_painting(&mut self, ui: &mut egui::Ui) {
let (rect, response) =
ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag());
self.angle += response.drag_delta().x * 0.01;
// Clone locals so we can move them into the paint callback:
let angle = self.angle;
// The callback function for WGPU is in two stages: prepare, and paint.
//
// The prepare callback is called every frame before paint and is given access to the wgpu
// Device and Queue, which can be used, for instance, to update buffers and uniforms before
// rendering.
//
// The paint callback is called after prepare and is given access to the render pass, which
// can be used to issue draw commands.
let cb = egui_wgpu::CallbackFn::new()
.prepare(move |device, queue, paint_callback_resources| {
let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap();
resources.prepare(device, queue, angle);
})
.paint(move |_info, rpass, paint_callback_resources| {
let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap();
resources.paint(rpass);
});
let callback = egui::PaintCallback {
rect,
callback: Arc::new(cb),
};
ui.painter().add(callback);
}
}
struct TriangleRenderResources {
pipeline: wgpu::RenderPipeline,
bind_group: wgpu::BindGroup,
uniform_buffer: wgpu::Buffer,
}
impl TriangleRenderResources {
fn prepare(&self, _device: &wgpu::Device, queue: &wgpu::Queue, angle: f32) {
// Update our uniform buffer with the angle from the UI
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[angle]));
}
fn paint<'rpass>(&'rpass self, rpass: &mut wgpu::RenderPass<'rpass>) {
// Draw our triangle!
rpass.set_pipeline(&self.pipeline);
rpass.set_bind_group(0, &self.bind_group, &[]);
rpass.draw(0..3, 0..1);
}
}

39
egui_demo_app/src/apps/custom3d_wgpu_shader.wgsl

@ -0,0 +1,39 @@
struct VertexOut {
[[location(0)]] color: vec4<f32>;
[[builtin(position)]] position: vec4<f32>;
};
struct Uniforms {
angle: f32;
};
[[group(0), binding(0)]]
var<uniform> uniforms: Uniforms;
var<private> v_positions: array<vec2<f32>, 3> = array<vec2<f32>, 3>(
vec2<f32>(0.0, 1.0),
vec2<f32>(1.0, -1.0),
vec2<f32>(-1.0, -1.0),
);
var<private> v_colors: array<vec4<f32>, 3> = array<vec4<f32>, 3>(
vec4<f32>(1.0, 0.0, 0.0, 1.0),
vec4<f32>(0.0, 1.0, 0.0, 1.0),
vec4<f32>(0.0, 0.0, 1.0, 1.0),
);
[[stage(vertex)]]
fn vs_main([[builtin(vertex_index)]] v_idx: u32) -> VertexOut {
var out: VertexOut;
out.position = vec4<f32>(v_positions[v_idx], 0.0, 1.0);
out.position.x = out.position.x * cos(uniforms.angle);
out.color = v_colors[v_idx];
return out;
}
[[stage(fragment)]]
fn fs_main(in: VertexOut) -> [[location(0)]] vec4<f32> {
return in.color;
}

14
egui_demo_app/src/apps/mod.rs

@ -1,13 +1,19 @@
#[cfg(feature = "glow")]
mod custom3d;
#[cfg(all(feature = "glow", not(feature = "wgpu")))]
mod custom3d_glow;
#[cfg(feature = "wgpu")]
mod custom3d_wgpu;
mod fractal_clock;
#[cfg(feature = "http")]
mod http_app;
#[cfg(feature = "glow")]
pub use custom3d::Custom3d;
#[cfg(all(feature = "glow", not(feature = "wgpu")))]
pub use custom3d_glow::Custom3d;
#[cfg(feature = "wgpu")]
pub use custom3d_wgpu::Custom3d;
pub use fractal_clock::FractalClock;

24
egui_demo_app/src/wrap_app.rs

@ -92,9 +92,7 @@ pub struct State {
/// Wraps many demo/test apps into one.
pub struct WrapApp {
state: State,
// not serialized (because it contains OpenGL buffers etc)
#[cfg(feature = "glow")]
custom3d: Option<crate::apps::Custom3d>,
custom3d: crate::apps::Custom3d,
dropped_files: Vec<egui::DroppedFile>,
}
@ -103,8 +101,7 @@ impl WrapApp {
#[allow(unused_mut)]
let mut slf = Self {
state: State::default(),
#[cfg(feature = "glow")]
custom3d: cc.gl.as_ref().map(|gl| crate::apps::Custom3d::new(gl)),
custom3d: crate::apps::Custom3d::new(cc),
dropped_files: Default::default(),
};
@ -149,14 +146,11 @@ impl WrapApp {
),
];
#[cfg(feature = "glow")]
if let Some(custom3d) = &mut self.custom3d {
vec.push((
"🔺 3D painting",
"custom3e",
custom3d as &mut dyn eframe::App,
));
}
vec.push((
"🔺 3D painting",
"custom3d",
&mut self.custom3d as &mut dyn eframe::App,
));
vec.push((
"🎨 Color test",
@ -224,9 +218,7 @@ impl eframe::App for WrapApp {
#[cfg(feature = "glow")]
fn on_exit(&mut self, gl: Option<&glow::Context>) {
if let Some(custom3d) = &mut self.custom3d {
custom3d.on_exit(gl);
}
self.custom3d.on_exit(gl);
}
}

2
egui_glow/src/lib.rs

@ -9,7 +9,7 @@
pub mod painter;
pub use glow;
pub use painter::Painter;
pub use painter::{CallbackFn, Painter};
mod misc_util;
mod post_process;
mod shader_version;

31
egui_glow/src/painter.rs

@ -4,7 +4,7 @@ use std::{collections::HashMap, sync::Arc};
use egui::{
emath::Rect,
epaint::{Color32, Mesh, Primitive, Vertex},
epaint::{Color32, Mesh, PaintCallbackInfo, Primitive, Vertex},
};
use glow::HasContext as _;
use memoffset::offset_of;
@ -68,6 +68,29 @@ pub struct Painter {
destroyed: bool,
}
/// A callback function that can be used to compose an [`egui::PaintCallback`] for custom rendering
/// with [`glow`].
///
/// The callback is passed, the [`egui::PaintCallbackInfo`] and the [`Painter`] which can be used to
/// access the OpenGL context.
///
/// # Example
///
/// See the [custom3d_glow] demo source for a detailed usage example.
///
/// [custom3d_glow]:
/// https://github.com/emilk/egui/blob/master/egui_demo_app/src/apps/custom3d_wgpu.rs
pub struct CallbackFn {
f: Box<dyn Fn(PaintCallbackInfo, &Painter) + Sync + Send>,
}
impl CallbackFn {
pub fn new<F: Fn(PaintCallbackInfo, &Painter) + Sync + Send + 'static>(callback: F) -> Self {
let f = Box::new(callback);
CallbackFn { f }
}
}
impl Painter {
/// Create painter.
///
@ -381,7 +404,11 @@ impl Painter {
screen_size_px,
};
callback.call(&info, self);
if let Some(callback) = callback.callback.downcast_ref::<CallbackFn>() {
(callback.f)(info, self);
} else {
tracing::warn!("Warning: Unsupported render callback. Expected egui_glow::CallbackFn");
}
check_for_gl_error!(&self.gl, "callback");

26
epaint/src/shape.rs

@ -1,6 +1,6 @@
//! The different shapes that can be painted.
use std::sync::Arc;
use std::{any::Any, sync::Arc};
use crate::{
text::{FontId, Fonts, Galley},
@ -747,21 +747,19 @@ pub struct PaintCallback {
/// Paint something custom (e.g. 3D stuff).
///
/// The argument is the render context, and what it contains depends on the backend.
/// In `eframe` it will be `egui_glow::Painter`.
/// The concrete value of `callback` depends on the rendering backend used. For instance, the
/// `glow` backend requires that callback be an `egui_glow::CallbackFn` while the `wgpu`
/// backend requires a `egui_wgpu::CallbackFn`.
///
/// The rendering backend is responsible for first setting the active viewport to [`Self::rect`].
/// If the type cannnot be downcast to the type expected by the current backend the callback
/// will not be drawn.
///
/// The rendering backend is also responsible for restoring any state,
/// such as the bound shader program and vertex array.
pub callback: Arc<dyn Fn(&PaintCallbackInfo, &mut dyn std::any::Any) + Send + Sync>,
}
impl PaintCallback {
#[inline]
pub fn call(&self, info: &PaintCallbackInfo, render_ctx: &mut dyn std::any::Any) {
(self.callback)(info, render_ctx);
}
/// The rendering backend is responsible for first setting the active viewport to
/// [`Self::rect`].
///
/// The rendering backend is also responsible for restoring any state, such as the bound shader
/// program, vertex array, etc.
pub callback: Arc<dyn Any + Sync + Send>,
}
impl std::fmt::Debug for PaintCallback {

10
examples/custom_3d_glow/src/main.rs

@ -76,13 +76,9 @@ impl MyApp {
let callback = egui::PaintCallback {
rect,
callback: std::sync::Arc::new(move |_info, render_ctx| {
if let Some(painter) = render_ctx.downcast_ref::<egui_glow::Painter>() {
rotating_triangle.lock().paint(painter.gl(), angle);
} else {
eprintln!("Can't do custom painting because we are not using a glow context");
}
}),
callback: std::sync::Arc::new(egui_glow::CallbackFn::new(move |_info, painter| {
rotating_triangle.lock().paint(painter.gl(), angle);
})),
};
ui.painter().add(callback);
}

14
examples/custom_3d_three-d/src/main.rs

@ -60,15 +60,11 @@ impl MyApp {
let callback = egui::PaintCallback {
rect,
callback: std::sync::Arc::new(move |info, render_ctx| {
if let Some(painter) = render_ctx.downcast_ref::<egui_glow::Painter>() {
with_three_d_context(painter.gl(), |three_d| {
paint_with_three_d(three_d, info, angle);
});
} else {
eprintln!("Can't do custom painting because we are not using a glow context");
}
}),
callback: std::sync::Arc::new(egui_glow::CallbackFn::new(move |info, painter| {
with_three_d_context(painter.gl(), |three_d| {
paint_with_three_d(three_d, &info, angle);
});
})),
};
ui.painter().add(callback);
}

Loading…
Cancel
Save