mirror of
https://github.com/emilk/egui.git
synced 2026-06-28 07:23:13 -04:00
Hey! I am not sure if this is something that's been considered before and decided against (I couldn't find any PR's or issues). This change removes the internal profiling macros in library crates and the `puffin` feature and replaces it with similar functions in the [profiling](https://github.com/aclysma/profiling) crate. This crate provides a layer of abstraction over various profiler instrumentation crates and allows library users to pick their favorite (supported) profiler. An additional benefit for puffin users is that dependencies of egui are included in the instrumentation output too (mainly wgpu which uses the profiling crate), so more details might be available when profiling. A breaking change is that instead of using the `puffin` feature on egui, users that want to profile the crate with puffin instead have to enable the `profile-with-puffin` feature on the profiling crate. Similarly they could instead choose to use `profile-with-tracy` etc. I tried to add a 'tracy' feature to egui_demo_app in order to showcase , however the /scripts/check.sh currently breaks on mutually exclusive features (which this introduces), so I decided against including it for the initial PR. I'm happy to iterate more on this if there is interest in taking this PR though. Screenshot showing the additional info for wgpu now available when using puffin 
1095 lines
43 KiB
Rust
1095 lines
43 KiB
Rust
#![allow(unsafe_code)]
|
|
|
|
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
|
|
|
|
use ahash::HashMap;
|
|
use epaint::{emath::NumExt, PaintCallbackInfo, Primitive, Vertex};
|
|
|
|
use wgpu::util::DeviceExt as _;
|
|
|
|
// Only implements Send + Sync on wasm32 in order to allow storing wgpu resources on the type map.
|
|
#[cfg(not(all(
|
|
target_arch = "wasm32",
|
|
not(feature = "fragile-send-sync-non-atomic-wasm"),
|
|
)))]
|
|
/// You can use this for storage when implementing [`CallbackTrait`].
|
|
pub type CallbackResources = type_map::concurrent::TypeMap;
|
|
#[cfg(all(
|
|
target_arch = "wasm32",
|
|
not(feature = "fragile-send-sync-non-atomic-wasm"),
|
|
))]
|
|
/// You can use this for storage when implementing [`CallbackTrait`].
|
|
pub type CallbackResources = type_map::TypeMap;
|
|
|
|
/// You can use this to do custom [`wgpu`] rendering in an egui app.
|
|
///
|
|
/// Implement [`CallbackTrait`] and call [`Callback::new_paint_callback`].
|
|
///
|
|
/// This can be turned into a [`epaint::PaintCallback`] and [`epaint::Shape`].
|
|
pub struct Callback(Box<dyn CallbackTrait>);
|
|
|
|
impl Callback {
|
|
/// Creates a new [`epaint::PaintCallback`] from a callback trait instance.
|
|
pub fn new_paint_callback(
|
|
rect: epaint::emath::Rect,
|
|
callback: impl CallbackTrait + 'static,
|
|
) -> epaint::PaintCallback {
|
|
epaint::PaintCallback {
|
|
rect,
|
|
callback: std::sync::Arc::new(Self(Box::new(callback))),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A callback trait that can be used to compose an [`epaint::PaintCallback`] via [`Callback`]
|
|
/// for custom WGPU rendering.
|
|
///
|
|
/// Callbacks in [`Renderer`] are done in three steps:
|
|
/// * [`CallbackTrait::prepare`]: called for all registered callbacks before the main egui render pass.
|
|
/// * [`CallbackTrait::finish_prepare`]: called for all registered callbacks after all callbacks finished calling prepare.
|
|
/// * [`CallbackTrait::paint`]: called for all registered callbacks during the main egui render pass.
|
|
///
|
|
/// Each callback has access to an instance of [`CallbackResources`] that is stored in the [`Renderer`].
|
|
/// This can be used to store wgpu resources that need to be accessed during the [`CallbackTrait::paint`] step.
|
|
///
|
|
/// The callbacks implementing [`CallbackTrait`] itself must always be Send + Sync, but resources stored in
|
|
/// [`Renderer::callback_resources`] are not required to implement Send + Sync when building for wasm.
|
|
/// (this is because wgpu stores references to the JS heap in most of its resources which can not be shared with other threads).
|
|
///
|
|
///
|
|
/// # Command submission
|
|
///
|
|
/// ## Command Encoder
|
|
///
|
|
/// The passed-in [`wgpu::CommandEncoder`] is egui's and can be used directly to register
|
|
/// wgpu commands for simple use cases.
|
|
/// This allows reusing the same [`wgpu::CommandEncoder`] for all callbacks and egui
|
|
/// rendering itself.
|
|
///
|
|
/// ## Command Buffers
|
|
///
|
|
/// For more complicated use cases, one can also return a list of arbitrary
|
|
/// [`wgpu::CommandBuffer`]s and have complete control over how they get created and fed.
|
|
/// In particular, this gives an opportunity to parallelize command registration and
|
|
/// prevents a faulty callback from poisoning the main wgpu pipeline.
|
|
///
|
|
/// When using eframe, the main egui command buffer, as well as all user-defined
|
|
/// command buffers returned by this function, are guaranteed to all be submitted
|
|
/// at once in a single call.
|
|
///
|
|
/// Command Buffers returned by [`CallbackTrait::finish_prepare`] will always be issued *after*
|
|
/// those returned by [`CallbackTrait::prepare`].
|
|
/// Order within command buffers returned by [`CallbackTrait::prepare`] is dependent
|
|
/// on the order the respective [`epaint::Shape::Callback`]s were submitted in.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/master/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example.
|
|
pub trait CallbackTrait: Send + Sync {
|
|
fn prepare(
|
|
&self,
|
|
_device: &wgpu::Device,
|
|
_queue: &wgpu::Queue,
|
|
_screen_descriptor: &ScreenDescriptor,
|
|
_egui_encoder: &mut wgpu::CommandEncoder,
|
|
_callback_resources: &mut CallbackResources,
|
|
) -> Vec<wgpu::CommandBuffer> {
|
|
Vec::new()
|
|
}
|
|
|
|
/// Called after all [`CallbackTrait::prepare`] calls are done.
|
|
fn finish_prepare(
|
|
&self,
|
|
_device: &wgpu::Device,
|
|
_queue: &wgpu::Queue,
|
|
_egui_encoder: &mut wgpu::CommandEncoder,
|
|
_callback_resources: &mut CallbackResources,
|
|
) -> Vec<wgpu::CommandBuffer> {
|
|
Vec::new()
|
|
}
|
|
|
|
/// Called after all [`CallbackTrait::finish_prepare`] calls are done.
|
|
///
|
|
/// It is given access to the [`wgpu::RenderPass`] so that it can issue draw commands
|
|
/// into the same [`wgpu::RenderPass`] that is used for all other egui elements.
|
|
fn paint(
|
|
&self,
|
|
info: PaintCallbackInfo,
|
|
render_pass: &mut wgpu::RenderPass<'static>,
|
|
callback_resources: &CallbackResources,
|
|
);
|
|
}
|
|
|
|
/// Information about the screen used for rendering.
|
|
pub struct ScreenDescriptor {
|
|
/// Size of the window in physical pixels.
|
|
pub size_in_pixels: [u32; 2],
|
|
|
|
/// High-DPI scale factor (pixels per point).
|
|
pub pixels_per_point: f32,
|
|
}
|
|
|
|
impl ScreenDescriptor {
|
|
/// size in "logical" points
|
|
fn screen_size_in_points(&self) -> [f32; 2] {
|
|
[
|
|
self.size_in_pixels[0] as f32 / self.pixels_per_point,
|
|
self.size_in_pixels[1] as f32 / self.pixels_per_point,
|
|
]
|
|
}
|
|
}
|
|
|
|
/// Uniform buffer used when rendering.
|
|
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
|
#[repr(C)]
|
|
struct UniformBuffer {
|
|
screen_size_in_points: [f32; 2],
|
|
dithering: u32,
|
|
// Uniform buffers need to be at least 16 bytes in WebGL.
|
|
// See https://github.com/gfx-rs/wgpu/issues/2072
|
|
_padding: u32,
|
|
}
|
|
|
|
impl PartialEq for UniformBuffer {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.screen_size_in_points == other.screen_size_in_points
|
|
&& self.dithering == other.dithering
|
|
}
|
|
}
|
|
|
|
struct SlicedBuffer {
|
|
buffer: wgpu::Buffer,
|
|
slices: Vec<Range<usize>>,
|
|
capacity: wgpu::BufferAddress,
|
|
}
|
|
|
|
pub struct Texture {
|
|
/// The texture may be None if the `TextureId` is just a handle to a user-provided bind-group.
|
|
pub texture: Option<wgpu::Texture>,
|
|
|
|
/// Bindgroup for the texture + sampler.
|
|
pub bind_group: wgpu::BindGroup,
|
|
|
|
/// Options describing the sampler used in the bind group. This may be None if the `TextureId`
|
|
/// is just a handle to a user-provided bind-group.
|
|
pub options: Option<epaint::textures::TextureOptions>,
|
|
}
|
|
|
|
/// Renderer for a egui based GUI.
|
|
pub struct Renderer {
|
|
pipeline: wgpu::RenderPipeline,
|
|
|
|
index_buffer: SlicedBuffer,
|
|
vertex_buffer: SlicedBuffer,
|
|
|
|
uniform_buffer: wgpu::Buffer,
|
|
previous_uniform_buffer_content: UniformBuffer,
|
|
uniform_bind_group: wgpu::BindGroup,
|
|
texture_bind_group_layout: wgpu::BindGroupLayout,
|
|
|
|
/// Map of egui texture IDs to textures and their associated bindgroups (texture view +
|
|
/// sampler). The texture may be None if the `TextureId` is just a handle to a user-provided
|
|
/// sampler.
|
|
textures: HashMap<epaint::TextureId, Texture>,
|
|
next_user_texture_id: u64,
|
|
samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>,
|
|
|
|
dithering: bool,
|
|
|
|
/// Storage for resources shared with all invocations of [`CallbackTrait`]'s methods.
|
|
///
|
|
/// See also [`CallbackTrait`].
|
|
pub callback_resources: CallbackResources,
|
|
}
|
|
|
|
impl Renderer {
|
|
/// Creates a renderer for a egui UI.
|
|
///
|
|
/// `output_color_format` should preferably be [`wgpu::TextureFormat::Rgba8Unorm`] or
|
|
/// [`wgpu::TextureFormat::Bgra8Unorm`], i.e. in gamma-space.
|
|
pub fn new(
|
|
device: &wgpu::Device,
|
|
output_color_format: wgpu::TextureFormat,
|
|
output_depth_format: Option<wgpu::TextureFormat>,
|
|
msaa_samples: u32,
|
|
dithering: bool,
|
|
) -> Self {
|
|
profiling::function_scope!();
|
|
|
|
let shader = wgpu::ShaderModuleDescriptor {
|
|
label: Some("egui"),
|
|
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("egui.wgsl"))),
|
|
};
|
|
let module = {
|
|
profiling::scope!("create_shader_module");
|
|
device.create_shader_module(shader)
|
|
};
|
|
|
|
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("egui_uniform_buffer"),
|
|
contents: bytemuck::cast_slice(&[UniformBuffer {
|
|
screen_size_in_points: [0.0, 0.0],
|
|
dithering: u32::from(dithering),
|
|
_padding: Default::default(),
|
|
}]),
|
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
});
|
|
|
|
let uniform_bind_group_layout = {
|
|
profiling::scope!("create_bind_group_layout");
|
|
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
label: Some("egui_uniform_bind_group_layout"),
|
|
entries: &[wgpu::BindGroupLayoutEntry {
|
|
binding: 0,
|
|
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
|
|
ty: wgpu::BindingType::Buffer {
|
|
has_dynamic_offset: false,
|
|
min_binding_size: NonZeroU64::new(std::mem::size_of::<UniformBuffer>() as _),
|
|
ty: wgpu::BufferBindingType::Uniform,
|
|
},
|
|
count: None,
|
|
}],
|
|
})
|
|
};
|
|
|
|
let uniform_bind_group = {
|
|
profiling::scope!("create_bind_group");
|
|
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("egui_uniform_bind_group"),
|
|
layout: &uniform_bind_group_layout,
|
|
entries: &[wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
|
buffer: &uniform_buffer,
|
|
offset: 0,
|
|
size: None,
|
|
}),
|
|
}],
|
|
})
|
|
};
|
|
|
|
let texture_bind_group_layout = {
|
|
profiling::scope!("create_bind_group_layout");
|
|
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
label: Some("egui_texture_bind_group_layout"),
|
|
entries: &[
|
|
wgpu::BindGroupLayoutEntry {
|
|
binding: 0,
|
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
ty: wgpu::BindingType::Texture {
|
|
multisampled: false,
|
|
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
|
view_dimension: wgpu::TextureViewDimension::D2,
|
|
},
|
|
count: None,
|
|
},
|
|
wgpu::BindGroupLayoutEntry {
|
|
binding: 1,
|
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
|
count: None,
|
|
},
|
|
],
|
|
})
|
|
};
|
|
|
|
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
|
label: Some("egui_pipeline_layout"),
|
|
bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout],
|
|
push_constant_ranges: &[],
|
|
});
|
|
|
|
let depth_stencil = output_depth_format.map(|format| wgpu::DepthStencilState {
|
|
format,
|
|
depth_write_enabled: false,
|
|
depth_compare: wgpu::CompareFunction::Always,
|
|
stencil: wgpu::StencilState::default(),
|
|
bias: wgpu::DepthBiasState::default(),
|
|
});
|
|
|
|
let pipeline = {
|
|
profiling::scope!("create_render_pipeline");
|
|
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
label: Some("egui_pipeline"),
|
|
layout: Some(&pipeline_layout),
|
|
vertex: wgpu::VertexState {
|
|
entry_point: Some("vs_main"),
|
|
module: &module,
|
|
buffers: &[wgpu::VertexBufferLayout {
|
|
array_stride: 5 * 4,
|
|
step_mode: wgpu::VertexStepMode::Vertex,
|
|
// 0: vec2 position
|
|
// 1: vec2 texture coordinates
|
|
// 2: uint color
|
|
attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32],
|
|
}],
|
|
compilation_options: wgpu::PipelineCompilationOptions::default()
|
|
},
|
|
primitive: wgpu::PrimitiveState {
|
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
|
unclipped_depth: false,
|
|
conservative: false,
|
|
cull_mode: None,
|
|
front_face: wgpu::FrontFace::default(),
|
|
polygon_mode: wgpu::PolygonMode::default(),
|
|
strip_index_format: None,
|
|
},
|
|
depth_stencil,
|
|
multisample: wgpu::MultisampleState {
|
|
alpha_to_coverage_enabled: false,
|
|
count: msaa_samples,
|
|
mask: !0,
|
|
},
|
|
|
|
fragment: Some(wgpu::FragmentState {
|
|
module: &module,
|
|
entry_point: Some(if output_color_format.is_srgb() {
|
|
log::warn!("Detected a linear (sRGBA aware) framebuffer {:?}. egui prefers Rgba8Unorm or Bgra8Unorm", output_color_format);
|
|
"fs_main_linear_framebuffer"
|
|
} else {
|
|
"fs_main_gamma_framebuffer" // this is what we prefer
|
|
}),
|
|
targets: &[Some(wgpu::ColorTargetState {
|
|
format: output_color_format,
|
|
blend: Some(wgpu::BlendState {
|
|
color: wgpu::BlendComponent {
|
|
src_factor: wgpu::BlendFactor::One,
|
|
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
|
|
operation: wgpu::BlendOperation::Add,
|
|
},
|
|
alpha: wgpu::BlendComponent {
|
|
src_factor: wgpu::BlendFactor::OneMinusDstAlpha,
|
|
dst_factor: wgpu::BlendFactor::One,
|
|
operation: wgpu::BlendOperation::Add,
|
|
},
|
|
}),
|
|
write_mask: wgpu::ColorWrites::ALL,
|
|
})],
|
|
compilation_options: wgpu::PipelineCompilationOptions::default()
|
|
}),
|
|
multiview: None,
|
|
cache: None,
|
|
}
|
|
)
|
|
};
|
|
|
|
const VERTEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
|
|
(std::mem::size_of::<Vertex>() * 1024) as _;
|
|
const INDEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
|
|
(std::mem::size_of::<u32>() * 1024 * 3) as _;
|
|
|
|
Self {
|
|
pipeline,
|
|
vertex_buffer: SlicedBuffer {
|
|
buffer: create_vertex_buffer(device, VERTEX_BUFFER_START_CAPACITY),
|
|
slices: Vec::with_capacity(64),
|
|
capacity: VERTEX_BUFFER_START_CAPACITY,
|
|
},
|
|
index_buffer: SlicedBuffer {
|
|
buffer: create_index_buffer(device, INDEX_BUFFER_START_CAPACITY),
|
|
slices: Vec::with_capacity(64),
|
|
capacity: INDEX_BUFFER_START_CAPACITY,
|
|
},
|
|
uniform_buffer,
|
|
// Buffers on wgpu are zero initialized, so this is indeed its current state!
|
|
previous_uniform_buffer_content: UniformBuffer {
|
|
screen_size_in_points: [0.0, 0.0],
|
|
dithering: 0,
|
|
_padding: 0,
|
|
},
|
|
uniform_bind_group,
|
|
texture_bind_group_layout,
|
|
textures: HashMap::default(),
|
|
next_user_texture_id: 0,
|
|
samplers: HashMap::default(),
|
|
dithering,
|
|
callback_resources: CallbackResources::default(),
|
|
}
|
|
}
|
|
|
|
/// Executes the egui renderer onto an existing wgpu renderpass.
|
|
///
|
|
/// Note that the lifetime of `render_pass` is `'static` which requires a call to [`wgpu::RenderPass::forget_lifetime`].
|
|
/// This allows users to pass resources that live outside of the callback resources to the render pass.
|
|
/// The render pass internally keeps all referenced resources alive as long as necessary.
|
|
/// The only consequence of `forget_lifetime` is that any operation on the parent encoder will cause a runtime error
|
|
/// instead of a compile time error.
|
|
pub fn render(
|
|
&self,
|
|
render_pass: &mut wgpu::RenderPass<'static>,
|
|
paint_jobs: &[epaint::ClippedPrimitive],
|
|
screen_descriptor: &ScreenDescriptor,
|
|
) {
|
|
profiling::function_scope!();
|
|
|
|
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 render pass because a paint callback has just
|
|
// 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();
|
|
|
|
for epaint::ClippedPrimitive {
|
|
clip_rect,
|
|
primitive,
|
|
} in paint_jobs
|
|
{
|
|
if needs_reset {
|
|
render_pass.set_viewport(
|
|
0.0,
|
|
0.0,
|
|
size_in_pixels[0] as f32,
|
|
size_in_pixels[1] as f32,
|
|
0.0,
|
|
1.0,
|
|
);
|
|
render_pass.set_pipeline(&self.pipeline);
|
|
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
|
needs_reset = false;
|
|
}
|
|
|
|
{
|
|
let rect = ScissorRect::new(clip_rect, pixels_per_point, size_in_pixels);
|
|
|
|
if rect.width == 0 || rect.height == 0 {
|
|
// Skip rendering zero-sized clip areas.
|
|
if let Primitive::Mesh(_) = primitive {
|
|
// If this is a mesh, we need to advance the index and vertex buffer iterators:
|
|
index_buffer_slices.next().unwrap();
|
|
vertex_buffer_slices.next().unwrap();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
render_pass.set_scissor_rect(rect.x, rect.y, rect.width, rect.height);
|
|
}
|
|
|
|
match primitive {
|
|
Primitive::Mesh(mesh) => {
|
|
let index_buffer_slice = index_buffer_slices.next().unwrap();
|
|
let vertex_buffer_slice = vertex_buffer_slices.next().unwrap();
|
|
|
|
if let Some(Texture { bind_group, .. }) = self.textures.get(&mesh.texture_id) {
|
|
render_pass.set_bind_group(1, bind_group, &[]);
|
|
render_pass.set_index_buffer(
|
|
self.index_buffer.buffer.slice(
|
|
index_buffer_slice.start as u64..index_buffer_slice.end as u64,
|
|
),
|
|
wgpu::IndexFormat::Uint32,
|
|
);
|
|
render_pass.set_vertex_buffer(
|
|
0,
|
|
self.vertex_buffer.buffer.slice(
|
|
vertex_buffer_slice.start as u64..vertex_buffer_slice.end as u64,
|
|
),
|
|
);
|
|
render_pass.draw_indexed(0..mesh.indices.len() as u32, 0, 0..1);
|
|
} else {
|
|
log::warn!("Missing texture: {:?}", mesh.texture_id);
|
|
}
|
|
}
|
|
Primitive::Callback(callback) => {
|
|
let Some(cbfn) = callback.callback.downcast_ref::<Callback>() else {
|
|
// We already warned in the `prepare` callback
|
|
continue;
|
|
};
|
|
|
|
let info = PaintCallbackInfo {
|
|
viewport: callback.rect,
|
|
clip_rect: *clip_rect,
|
|
pixels_per_point,
|
|
screen_size_px: size_in_pixels,
|
|
};
|
|
|
|
let viewport_px = info.viewport_in_pixels();
|
|
if viewport_px.width_px > 0 && viewport_px.height_px > 0 {
|
|
profiling::scope!("callback");
|
|
|
|
needs_reset = true;
|
|
|
|
// We're setting a default viewport for the render pass as a
|
|
// courtesy for the user, so that they don't have to think about
|
|
// it in the simple case where they just want to fill the whole
|
|
// paint area.
|
|
//
|
|
// The user still has the possibility of setting their own custom
|
|
// viewport during the paint callback, effectively overriding this
|
|
// one.
|
|
render_pass.set_viewport(
|
|
viewport_px.left_px as f32,
|
|
viewport_px.top_px as f32,
|
|
viewport_px.width_px as f32,
|
|
viewport_px.height_px as f32,
|
|
0.0,
|
|
1.0,
|
|
);
|
|
|
|
cbfn.0.paint(info, render_pass, &self.callback_resources);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
render_pass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]);
|
|
}
|
|
|
|
/// Should be called before [`Self::render`].
|
|
pub fn update_texture(
|
|
&mut self,
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
id: epaint::TextureId,
|
|
image_delta: &epaint::ImageDelta,
|
|
) {
|
|
profiling::function_scope!();
|
|
|
|
let width = image_delta.image.width() as u32;
|
|
let height = image_delta.image.height() as u32;
|
|
|
|
let size = wgpu::Extent3d {
|
|
width,
|
|
height,
|
|
depth_or_array_layers: 1,
|
|
};
|
|
|
|
let data_color32 = match &image_delta.image {
|
|
epaint::ImageData::Color(image) => {
|
|
assert_eq!(
|
|
width as usize * height as usize,
|
|
image.pixels.len(),
|
|
"Mismatch between texture size and texel count"
|
|
);
|
|
Cow::Borrowed(&image.pixels)
|
|
}
|
|
epaint::ImageData::Font(image) => {
|
|
assert_eq!(
|
|
width as usize * height as usize,
|
|
image.pixels.len(),
|
|
"Mismatch between texture size and texel count"
|
|
);
|
|
profiling::scope!("font -> sRGBA");
|
|
Cow::Owned(image.srgba_pixels(None).collect::<Vec<epaint::Color32>>())
|
|
}
|
|
};
|
|
let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice());
|
|
|
|
let queue_write_data_to_texture = |texture, origin| {
|
|
profiling::scope!("write_texture");
|
|
queue.write_texture(
|
|
wgpu::ImageCopyTexture {
|
|
texture,
|
|
mip_level: 0,
|
|
origin,
|
|
aspect: wgpu::TextureAspect::All,
|
|
},
|
|
data_bytes,
|
|
wgpu::ImageDataLayout {
|
|
offset: 0,
|
|
bytes_per_row: Some(4 * width),
|
|
rows_per_image: Some(height),
|
|
},
|
|
size,
|
|
);
|
|
};
|
|
|
|
// Use same label for all resources associated with this texture id (no point in retyping the type)
|
|
let label_str = format!("egui_texid_{id:?}");
|
|
let label = Some(label_str.as_str());
|
|
|
|
let (texture, origin, bind_group) = if let Some(pos) = image_delta.pos {
|
|
// update the existing texture
|
|
let Texture {
|
|
texture,
|
|
bind_group,
|
|
options,
|
|
} = self
|
|
.textures
|
|
.remove(&id)
|
|
.expect("Tried to update a texture that has not been allocated yet.");
|
|
let texture = texture.expect("Tried to update user texture.");
|
|
let options = options.expect("Tried to update user texture.");
|
|
let origin = wgpu::Origin3d {
|
|
x: pos[0] as u32,
|
|
y: pos[1] as u32,
|
|
z: 0,
|
|
};
|
|
|
|
(
|
|
texture,
|
|
origin,
|
|
// If the TextureOptions are the same as the previous ones, we can reuse the bind group. Otherwise we
|
|
// have to recreate it.
|
|
if image_delta.options == options {
|
|
Some(bind_group)
|
|
} else {
|
|
None
|
|
},
|
|
)
|
|
} else {
|
|
// allocate a new texture
|
|
let texture = {
|
|
profiling::scope!("create_texture");
|
|
device.create_texture(&wgpu::TextureDescriptor {
|
|
label,
|
|
size,
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: wgpu::TextureDimension::D2,
|
|
format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported.
|
|
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
|
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
|
|
})
|
|
};
|
|
let origin = wgpu::Origin3d::ZERO;
|
|
(texture, origin, None)
|
|
};
|
|
|
|
let bind_group = bind_group.unwrap_or_else(|| {
|
|
let sampler = self
|
|
.samplers
|
|
.entry(image_delta.options)
|
|
.or_insert_with(|| create_sampler(image_delta.options, device));
|
|
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label,
|
|
layout: &self.texture_bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: wgpu::BindingResource::TextureView(
|
|
&texture.create_view(&wgpu::TextureViewDescriptor::default()),
|
|
),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: wgpu::BindingResource::Sampler(sampler),
|
|
},
|
|
],
|
|
})
|
|
});
|
|
|
|
queue_write_data_to_texture(&texture, origin);
|
|
self.textures.insert(
|
|
id,
|
|
Texture {
|
|
texture: Some(texture),
|
|
bind_group,
|
|
options: Some(image_delta.options),
|
|
},
|
|
);
|
|
}
|
|
|
|
pub fn free_texture(&mut self, id: &epaint::TextureId) {
|
|
if let Some(texture) = self.textures.remove(id).and_then(|t| t.texture) {
|
|
texture.destroy();
|
|
}
|
|
}
|
|
|
|
/// Get the WGPU texture and bind group associated to a texture that has been allocated by egui.
|
|
///
|
|
/// This could be used by custom paint hooks to render images that have been added through
|
|
/// [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
|
|
pub fn texture(&self, id: &epaint::TextureId) -> Option<&Texture> {
|
|
self.textures.get(id)
|
|
}
|
|
|
|
/// Registers a [`wgpu::Texture`] with a [`epaint::TextureId`].
|
|
///
|
|
/// This enables the application to reference the texture inside an image ui element.
|
|
/// This effectively enables off-screen rendering inside the egui UI. Texture must have
|
|
/// the texture format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
|
|
pub fn register_native_texture(
|
|
&mut self,
|
|
device: &wgpu::Device,
|
|
texture: &wgpu::TextureView,
|
|
texture_filter: wgpu::FilterMode,
|
|
) -> epaint::TextureId {
|
|
self.register_native_texture_with_sampler_options(
|
|
device,
|
|
texture,
|
|
wgpu::SamplerDescriptor {
|
|
label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
|
|
mag_filter: texture_filter,
|
|
min_filter: texture_filter,
|
|
..Default::default()
|
|
},
|
|
)
|
|
}
|
|
|
|
/// Registers a [`wgpu::Texture`] with an existing [`epaint::TextureId`].
|
|
///
|
|
/// This enables applications to reuse [`epaint::TextureId`]s.
|
|
pub fn update_egui_texture_from_wgpu_texture(
|
|
&mut self,
|
|
device: &wgpu::Device,
|
|
texture: &wgpu::TextureView,
|
|
texture_filter: wgpu::FilterMode,
|
|
id: epaint::TextureId,
|
|
) {
|
|
self.update_egui_texture_from_wgpu_texture_with_sampler_options(
|
|
device,
|
|
texture,
|
|
wgpu::SamplerDescriptor {
|
|
label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
|
|
mag_filter: texture_filter,
|
|
min_filter: texture_filter,
|
|
..Default::default()
|
|
},
|
|
id,
|
|
);
|
|
}
|
|
|
|
/// Registers a [`wgpu::Texture`] with a [`epaint::TextureId`] while also accepting custom
|
|
/// [`wgpu::SamplerDescriptor`] options.
|
|
///
|
|
/// This allows applications to specify individual minification/magnification filters as well as
|
|
/// custom mipmap and tiling options.
|
|
///
|
|
/// The texture must have the format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
|
|
/// Any compare function supplied in the [`wgpu::SamplerDescriptor`] will be ignored.
|
|
#[allow(clippy::needless_pass_by_value)] // false positive
|
|
pub fn register_native_texture_with_sampler_options(
|
|
&mut self,
|
|
device: &wgpu::Device,
|
|
texture: &wgpu::TextureView,
|
|
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
|
|
) -> epaint::TextureId {
|
|
profiling::function_scope!();
|
|
|
|
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
|
compare: None,
|
|
..sampler_descriptor
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
|
|
layout: &self.texture_bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: wgpu::BindingResource::TextureView(texture),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: wgpu::BindingResource::Sampler(&sampler),
|
|
},
|
|
],
|
|
});
|
|
|
|
let id = epaint::TextureId::User(self.next_user_texture_id);
|
|
self.textures.insert(
|
|
id,
|
|
Texture {
|
|
texture: None,
|
|
bind_group,
|
|
options: None,
|
|
},
|
|
);
|
|
self.next_user_texture_id += 1;
|
|
|
|
id
|
|
}
|
|
|
|
/// Registers a [`wgpu::Texture`] with an existing [`epaint::TextureId`] while also accepting custom
|
|
/// [`wgpu::SamplerDescriptor`] options.
|
|
///
|
|
/// This allows applications to reuse [`epaint::TextureId`]s created with custom sampler options.
|
|
#[allow(clippy::needless_pass_by_value)] // false positive
|
|
pub fn update_egui_texture_from_wgpu_texture_with_sampler_options(
|
|
&mut self,
|
|
device: &wgpu::Device,
|
|
texture: &wgpu::TextureView,
|
|
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
|
|
id: epaint::TextureId,
|
|
) {
|
|
profiling::function_scope!();
|
|
|
|
let Texture {
|
|
bind_group: user_texture_binding,
|
|
..
|
|
} = self
|
|
.textures
|
|
.get_mut(&id)
|
|
.expect("Tried to update a texture that has not been allocated yet.");
|
|
|
|
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
|
compare: None,
|
|
..sampler_descriptor
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
|
|
layout: &self.texture_bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: wgpu::BindingResource::TextureView(texture),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: wgpu::BindingResource::Sampler(&sampler),
|
|
},
|
|
],
|
|
});
|
|
|
|
*user_texture_binding = bind_group;
|
|
}
|
|
|
|
/// Uploads the uniform, vertex and index data used by the renderer.
|
|
/// Should be called before [`Self::render`].
|
|
///
|
|
/// Returns all user-defined command buffers gathered from [`CallbackTrait::prepare`] & [`CallbackTrait::finish_prepare`] callbacks.
|
|
pub fn update_buffers(
|
|
&mut self,
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
encoder: &mut wgpu::CommandEncoder,
|
|
paint_jobs: &[epaint::ClippedPrimitive],
|
|
screen_descriptor: &ScreenDescriptor,
|
|
) -> Vec<wgpu::CommandBuffer> {
|
|
profiling::function_scope!();
|
|
|
|
let screen_size_in_points = screen_descriptor.screen_size_in_points();
|
|
|
|
let uniform_buffer_content = UniformBuffer {
|
|
screen_size_in_points,
|
|
dithering: u32::from(self.dithering),
|
|
_padding: Default::default(),
|
|
};
|
|
if uniform_buffer_content != self.previous_uniform_buffer_content {
|
|
profiling::scope!("update uniforms");
|
|
queue.write_buffer(
|
|
&self.uniform_buffer,
|
|
0,
|
|
bytemuck::cast_slice(&[uniform_buffer_content]),
|
|
);
|
|
self.previous_uniform_buffer_content = uniform_buffer_content;
|
|
}
|
|
|
|
// Determine how many vertices & indices need to be rendered, and gather prepare callbacks
|
|
let mut callbacks = Vec::new();
|
|
let (vertex_count, index_count) = {
|
|
profiling::scope!("count_vertices_indices");
|
|
paint_jobs.iter().fold((0, 0), |acc, clipped_primitive| {
|
|
match &clipped_primitive.primitive {
|
|
Primitive::Mesh(mesh) => {
|
|
(acc.0 + mesh.vertices.len(), acc.1 + mesh.indices.len())
|
|
}
|
|
Primitive::Callback(callback) => {
|
|
if let Some(c) = callback.callback.downcast_ref::<Callback>() {
|
|
callbacks.push(c.0.as_ref());
|
|
} else {
|
|
log::warn!("Unknown paint callback: expected `egui_wgpu::Callback`");
|
|
};
|
|
acc
|
|
}
|
|
}
|
|
})
|
|
};
|
|
|
|
if index_count > 0 {
|
|
profiling::scope!("indices", index_count.to_string().as_str());
|
|
|
|
self.index_buffer.slices.clear();
|
|
|
|
let required_index_buffer_size = (std::mem::size_of::<u32>() * index_count) as u64;
|
|
if self.index_buffer.capacity < required_index_buffer_size {
|
|
// Resize index buffer if needed.
|
|
self.index_buffer.capacity =
|
|
(self.index_buffer.capacity * 2).at_least(required_index_buffer_size);
|
|
self.index_buffer.buffer = create_index_buffer(device, self.index_buffer.capacity);
|
|
}
|
|
|
|
let index_buffer_staging = queue.write_buffer_with(
|
|
&self.index_buffer.buffer,
|
|
0,
|
|
NonZeroU64::new(required_index_buffer_size).unwrap(),
|
|
);
|
|
|
|
let Some(mut index_buffer_staging) = index_buffer_staging else {
|
|
panic!("Failed to create staging buffer for index data. Index count: {index_count}. Required index buffer size: {required_index_buffer_size}. Actual size {} and capacity: {} (bytes)", self.index_buffer.buffer.size(), self.index_buffer.capacity);
|
|
};
|
|
|
|
let mut index_offset = 0;
|
|
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs {
|
|
match primitive {
|
|
Primitive::Mesh(mesh) => {
|
|
let size = mesh.indices.len() * std::mem::size_of::<u32>();
|
|
let slice = index_offset..(size + index_offset);
|
|
index_buffer_staging[slice.clone()]
|
|
.copy_from_slice(bytemuck::cast_slice(&mesh.indices));
|
|
self.index_buffer.slices.push(slice);
|
|
index_offset += size;
|
|
}
|
|
Primitive::Callback(_) => {}
|
|
}
|
|
}
|
|
}
|
|
if vertex_count > 0 {
|
|
profiling::scope!("vertices", vertex_count.to_string().as_str());
|
|
|
|
self.vertex_buffer.slices.clear();
|
|
|
|
let required_vertex_buffer_size = (std::mem::size_of::<Vertex>() * vertex_count) as u64;
|
|
if self.vertex_buffer.capacity < required_vertex_buffer_size {
|
|
// Resize vertex buffer if needed.
|
|
self.vertex_buffer.capacity =
|
|
(self.vertex_buffer.capacity * 2).at_least(required_vertex_buffer_size);
|
|
self.vertex_buffer.buffer =
|
|
create_vertex_buffer(device, self.vertex_buffer.capacity);
|
|
}
|
|
|
|
let vertex_buffer_staging = queue.write_buffer_with(
|
|
&self.vertex_buffer.buffer,
|
|
0,
|
|
NonZeroU64::new(required_vertex_buffer_size).unwrap(),
|
|
);
|
|
|
|
let Some(mut vertex_buffer_staging) = vertex_buffer_staging else {
|
|
panic!("Failed to create staging buffer for vertex data. Vertex count: {vertex_count}. Required vertex buffer size: {required_vertex_buffer_size}. Actual size {} and capacity: {} (bytes)", self.vertex_buffer.buffer.size(), self.vertex_buffer.capacity);
|
|
};
|
|
|
|
let mut vertex_offset = 0;
|
|
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs {
|
|
match primitive {
|
|
Primitive::Mesh(mesh) => {
|
|
let size = mesh.vertices.len() * std::mem::size_of::<Vertex>();
|
|
let slice = vertex_offset..(size + vertex_offset);
|
|
vertex_buffer_staging[slice.clone()]
|
|
.copy_from_slice(bytemuck::cast_slice(&mesh.vertices));
|
|
self.vertex_buffer.slices.push(slice);
|
|
vertex_offset += size;
|
|
}
|
|
Primitive::Callback(_) => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut user_cmd_bufs = Vec::new();
|
|
{
|
|
profiling::scope!("prepare callbacks");
|
|
for callback in &callbacks {
|
|
user_cmd_bufs.extend(callback.prepare(
|
|
device,
|
|
queue,
|
|
screen_descriptor,
|
|
encoder,
|
|
&mut self.callback_resources,
|
|
));
|
|
}
|
|
}
|
|
{
|
|
profiling::scope!("finish prepare callbacks");
|
|
for callback in &callbacks {
|
|
user_cmd_bufs.extend(callback.finish_prepare(
|
|
device,
|
|
queue,
|
|
encoder,
|
|
&mut self.callback_resources,
|
|
));
|
|
}
|
|
}
|
|
|
|
user_cmd_bufs
|
|
}
|
|
}
|
|
|
|
fn create_sampler(
|
|
options: epaint::textures::TextureOptions,
|
|
device: &wgpu::Device,
|
|
) -> wgpu::Sampler {
|
|
let mag_filter = match options.magnification {
|
|
epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
|
|
epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
|
|
};
|
|
let min_filter = match options.minification {
|
|
epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
|
|
epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
|
|
};
|
|
let address_mode = match options.wrap_mode {
|
|
epaint::textures::TextureWrapMode::ClampToEdge => wgpu::AddressMode::ClampToEdge,
|
|
epaint::textures::TextureWrapMode::Repeat => wgpu::AddressMode::Repeat,
|
|
epaint::textures::TextureWrapMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat,
|
|
};
|
|
device.create_sampler(&wgpu::SamplerDescriptor {
|
|
label: Some(&format!(
|
|
"egui sampler (mag: {mag_filter:?}, min {min_filter:?})"
|
|
)),
|
|
mag_filter,
|
|
min_filter,
|
|
address_mode_u: address_mode,
|
|
address_mode_v: address_mode,
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
fn create_vertex_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
|
|
profiling::function_scope!();
|
|
device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("egui_vertex_buffer"),
|
|
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
|
size,
|
|
mapped_at_creation: false,
|
|
})
|
|
}
|
|
|
|
fn create_index_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
|
|
profiling::function_scope!();
|
|
device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("egui_index_buffer"),
|
|
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
|
|
size,
|
|
mapped_at_creation: false,
|
|
})
|
|
}
|
|
|
|
/// A Rect in physical pixel space, used for setting clipping rectangles.
|
|
struct ScissorRect {
|
|
x: u32,
|
|
y: u32,
|
|
width: u32,
|
|
height: u32,
|
|
}
|
|
|
|
impl ScissorRect {
|
|
fn new(clip_rect: &epaint::Rect, pixels_per_point: f32, target_size: [u32; 2]) -> Self {
|
|
// 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;
|
|
|
|
// Round to integer:
|
|
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;
|
|
|
|
// Clamp:
|
|
let clip_min_x = clip_min_x.clamp(0, target_size[0]);
|
|
let clip_min_y = clip_min_y.clamp(0, target_size[1]);
|
|
let clip_max_x = clip_max_x.clamp(clip_min_x, target_size[0]);
|
|
let clip_max_y = clip_max_y.clamp(clip_min_y, target_size[1]);
|
|
|
|
Self {
|
|
x: clip_min_x,
|
|
y: clip_min_y,
|
|
width: clip_max_x - clip_min_x,
|
|
height: clip_max_y - clip_min_y,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Look at the feature flag for an explanation.
|
|
#[cfg(not(all(
|
|
target_arch = "wasm32",
|
|
not(feature = "fragile-send-sync-non-atomic-wasm"),
|
|
)))]
|
|
#[test]
|
|
fn renderer_impl_send_sync() {
|
|
fn assert_send_sync<T: Send + Sync>() {}
|
|
assert_send_sync::<Renderer>();
|
|
}
|