1use egui::epaint::{ClippedPrimitive, ImageDelta, Primitive, Vertex};
22use egui::{ImageData, TextureId, TexturesDelta};
23use std::collections::HashMap;
24use wgpu::util::DeviceExt;
25
26pub struct EguiRenderState<'a> {
28 pub device: &'a wgpu::Device,
30 pub queue: &'a wgpu::Queue,
32 pub encoder: &'a mut wgpu::CommandEncoder,
34 pub target_view: &'a wgpu::TextureView,
36 pub width_px: u32,
38 pub height_px: u32,
40}
41
42pub struct EguiWgpuRenderer {
44 pipeline: Option<wgpu::RenderPipeline>,
45 screen_uniform_buffer: Option<wgpu::Buffer>,
46 screen_bind_group: Option<wgpu::BindGroup>,
47 screen_bind_group_layout: Option<wgpu::BindGroupLayout>,
48 texture_bind_group_layout: Option<wgpu::BindGroupLayout>,
49 sampler: Option<wgpu::Sampler>,
50 textures: HashMap<TextureId, (wgpu::Texture, wgpu::BindGroup)>,
51 surface_format: wgpu::TextureFormat,
52 next_user_texture_id: u64,
53}
54
55impl EguiWgpuRenderer {
56 pub fn new(surface_format: wgpu::TextureFormat) -> Self {
58 Self {
59 pipeline: None,
60 screen_uniform_buffer: None,
61 screen_bind_group: None,
62 screen_bind_group_layout: None,
63 texture_bind_group_layout: None,
64 sampler: None,
65 textures: HashMap::new(),
66 surface_format,
67 next_user_texture_id: 0,
68 }
69 }
70
71 pub fn initialize(&mut self, device: &wgpu::Device, shader_source: &str) {
75 let screen_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
77 label: Some("egui_screen_bgl"),
78 entries: &[wgpu::BindGroupLayoutEntry {
79 binding: 0,
80 visibility: wgpu::ShaderStages::VERTEX,
81 ty: wgpu::BindingType::Buffer {
82 ty: wgpu::BufferBindingType::Uniform,
83 has_dynamic_offset: false,
84 min_binding_size: Some(std::num::NonZero::new(8).unwrap()),
85 },
86 count: None,
87 }],
88 });
89
90 let texture_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
92 label: Some("egui_texture_bgl"),
93 entries: &[
94 wgpu::BindGroupLayoutEntry {
95 binding: 0,
96 visibility: wgpu::ShaderStages::FRAGMENT,
97 ty: wgpu::BindingType::Texture {
98 sample_type: wgpu::TextureSampleType::Float { filterable: true },
99 view_dimension: wgpu::TextureViewDimension::D2,
100 multisampled: false,
101 },
102 count: None,
103 },
104 wgpu::BindGroupLayoutEntry {
105 binding: 1,
106 visibility: wgpu::ShaderStages::FRAGMENT,
107 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
108 count: None,
109 },
110 ],
111 });
112
113 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
115 label: Some("egui_pipeline_layout"),
116 bind_group_layouts: &[Some(&screen_bgl), Some(&texture_bgl)],
117 immediate_size: 0,
118 });
119
120 let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
122 label: Some("egui_shader"),
123 source: wgpu::ShaderSource::Wgsl(shader_source.into()),
124 });
125
126 let vertex_buffer_layout = wgpu::VertexBufferLayout {
130 array_stride: std::mem::size_of::<Vertex>() as u64,
131 step_mode: wgpu::VertexStepMode::Vertex,
132 attributes: &[
133 wgpu::VertexAttribute {
135 format: wgpu::VertexFormat::Float32x2,
136 offset: 0,
137 shader_location: 0,
138 },
139 wgpu::VertexAttribute {
141 format: wgpu::VertexFormat::Float32x2,
142 offset: 8,
143 shader_location: 1,
144 },
145 wgpu::VertexAttribute {
147 format: wgpu::VertexFormat::Unorm8x4,
148 offset: 16,
149 shader_location: 2,
150 },
151 ],
152 };
153
154 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
156 label: Some("egui_pipeline"),
157 layout: Some(&pipeline_layout),
158 vertex: wgpu::VertexState {
159 module: &shader_module,
160 entry_point: Some("vs_main"),
161 buffers: &[vertex_buffer_layout],
162 compilation_options: Default::default(),
163 },
164 primitive: wgpu::PrimitiveState {
165 topology: wgpu::PrimitiveTopology::TriangleList,
166 strip_index_format: None,
167 front_face: wgpu::FrontFace::Ccw,
168 cull_mode: None, unclipped_depth: false,
170 polygon_mode: wgpu::PolygonMode::Fill,
171 conservative: false,
172 },
173 depth_stencil: None,
174 multisample: wgpu::MultisampleState::default(),
175 fragment: Some(wgpu::FragmentState {
176 module: &shader_module,
177 entry_point: Some("fs_main"),
178 targets: &[Some(wgpu::ColorTargetState {
179 format: self.surface_format,
180 blend: Some(wgpu::BlendState {
181 color: wgpu::BlendComponent {
182 src_factor: wgpu::BlendFactor::One,
183 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
184 operation: wgpu::BlendOperation::Add,
185 },
186 alpha: wgpu::BlendComponent {
187 src_factor: wgpu::BlendFactor::OneMinusDstAlpha,
188 dst_factor: wgpu::BlendFactor::One,
189 operation: wgpu::BlendOperation::Add,
190 },
191 }),
192 write_mask: wgpu::ColorWrites::ALL,
193 })],
194 compilation_options: Default::default(),
195 }),
196 multiview_mask: None,
197 cache: None,
198 });
199
200 let screen_buffer = device.create_buffer(&wgpu::BufferDescriptor {
202 label: Some("egui_screen_uniform"),
203 size: 8,
204 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
205 mapped_at_creation: false,
206 });
207
208 let screen_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
209 label: Some("egui_screen_bg"),
210 layout: &screen_bgl,
211 entries: &[wgpu::BindGroupEntry {
212 binding: 0,
213 resource: screen_buffer.as_entire_binding(),
214 }],
215 });
216
217 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
219 label: Some("egui_sampler"),
220 mag_filter: wgpu::FilterMode::Linear,
221 min_filter: wgpu::FilterMode::Linear,
222 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
223 address_mode_u: wgpu::AddressMode::ClampToEdge,
224 address_mode_v: wgpu::AddressMode::ClampToEdge,
225 ..Default::default()
226 });
227
228 self.pipeline = Some(pipeline);
229 self.screen_uniform_buffer = Some(screen_buffer);
230 self.screen_bind_group = Some(screen_bind_group);
231 self.screen_bind_group_layout = Some(screen_bgl);
232 self.texture_bind_group_layout = Some(texture_bgl);
233 self.sampler = Some(sampler);
234
235 log::info!(
236 "EguiWgpuRenderer: Initialized with format {:?}",
237 self.surface_format
238 );
239 }
240
241 pub fn update_textures(
243 &mut self,
244 device: &wgpu::Device,
245 queue: &wgpu::Queue,
246 textures_delta: &TexturesDelta,
247 ) {
248 for (id, delta) in &textures_delta.set {
249 self.set_texture(device, queue, *id, delta);
250 }
251
252 for id in &textures_delta.free {
253 self.textures.remove(id);
254 }
255 }
256
257 pub fn register_external_texture(
262 &mut self,
263 device: &wgpu::Device,
264 view: &wgpu::TextureView,
265 ) -> TextureId {
266 let (Some(texture_bgl), Some(sampler)) = (
267 self.texture_bind_group_layout.as_ref(),
268 self.sampler.as_ref(),
269 ) else {
270 let id = TextureId::User(self.next_user_texture_id);
274 self.next_user_texture_id += 1;
275 log::error!(
276 "EguiWgpuRenderer: register_external_texture called before initialize(); \
277 returning unbacked texture id {id:?}"
278 );
279 return id;
280 };
281
282 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
283 label: Some("egui_external_texture_bg"),
284 layout: texture_bgl,
285 entries: &[
286 wgpu::BindGroupEntry {
287 binding: 0,
288 resource: wgpu::BindingResource::TextureView(view),
289 },
290 wgpu::BindGroupEntry {
291 binding: 1,
292 resource: wgpu::BindingResource::Sampler(sampler),
293 },
294 ],
295 });
296
297 let id = TextureId::User(self.next_user_texture_id);
299 self.next_user_texture_id += 1;
300
301 let placeholder = device.create_texture(&wgpu::TextureDescriptor {
304 label: Some("egui_ext_placeholder"),
305 size: wgpu::Extent3d {
306 width: 1,
307 height: 1,
308 depth_or_array_layers: 1,
309 },
310 mip_level_count: 1,
311 sample_count: 1,
312 dimension: wgpu::TextureDimension::D2,
313 format: wgpu::TextureFormat::Rgba8UnormSrgb,
314 usage: wgpu::TextureUsages::TEXTURE_BINDING,
315 view_formats: &[],
316 });
317
318 self.textures.insert(id, (placeholder, bind_group));
319 id
320 }
321
322 pub fn update_external_texture(
326 &mut self,
327 device: &wgpu::Device,
328 id: TextureId,
329 view: &wgpu::TextureView,
330 ) {
331 let (Some(texture_bgl), Some(sampler)) = (
332 self.texture_bind_group_layout.as_ref(),
333 self.sampler.as_ref(),
334 ) else {
335 log::error!(
336 "EguiWgpuRenderer: update_external_texture called before initialize(); skipping"
337 );
338 return;
339 };
340
341 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
342 label: Some("egui_external_texture_bg_updated"),
343 layout: texture_bgl,
344 entries: &[
345 wgpu::BindGroupEntry {
346 binding: 0,
347 resource: wgpu::BindingResource::TextureView(view),
348 },
349 wgpu::BindGroupEntry {
350 binding: 1,
351 resource: wgpu::BindingResource::Sampler(sampler),
352 },
353 ],
354 });
355
356 if let Some(entry) = self.textures.get_mut(&id) {
357 entry.1 = bind_group;
358 }
359 }
360
361 pub fn render(
363 &self,
364 state: &mut EguiRenderState<'_>,
365 clipped_primitives: &[ClippedPrimitive],
366 pixels_per_point: f32,
367 ) {
368 let pipeline = match &self.pipeline {
369 Some(p) => p,
370 None => {
371 log::warn!("EguiWgpuRenderer: Cannot render — not initialized");
372 return;
373 }
374 };
375
376 let (Some(screen_bg), Some(screen_buf)) = (
381 self.screen_bind_group.as_ref(),
382 self.screen_uniform_buffer.as_ref(),
383 ) else {
384 log::error!("EguiWgpuRenderer: screen resources missing despite initialized pipeline");
385 return;
386 };
387
388 let screen_size = [state.width_px as f32, state.height_px as f32];
390 state
391 .queue
392 .write_buffer(screen_buf, 0, bytemuck::cast_slice(&screen_size));
393
394 let mut vertices: Vec<u8> = Vec::new();
396 let mut indices: Vec<u32> = Vec::new();
397 let mut draw_calls: Vec<DrawCall> = Vec::new();
398
399 for primitive in clipped_primitives {
400 match &primitive.primitive {
401 Primitive::Mesh(mesh) => {
402 if mesh.vertices.is_empty() || mesh.indices.is_empty() {
403 continue;
404 }
405
406 let vertex_offset = vertices.len() / std::mem::size_of::<Vertex>();
407 let index_offset = indices.len();
408
409 let vertex_bytes: &[u8] = bytemuck::cast_slice(&mesh.vertices);
411 vertices.extend_from_slice(vertex_bytes);
412
413 for &idx in &mesh.indices {
415 indices.push(idx + vertex_offset as u32);
416 }
417
418 let clip = primitive.clip_rect;
420 let x = (clip.min.x * pixels_per_point).round().max(0.0) as u32;
421 let y = (clip.min.y * pixels_per_point).round().max(0.0) as u32;
422 let w = ((clip.max.x - clip.min.x) * pixels_per_point)
423 .round()
424 .max(1.0) as u32;
425 let h = ((clip.max.y - clip.min.y) * pixels_per_point)
426 .round()
427 .max(1.0) as u32;
428
429 let x = x.min(state.width_px.saturating_sub(1));
431 let y = y.min(state.height_px.saturating_sub(1));
432 let w = w.min(state.width_px.saturating_sub(x));
433 let h = h.min(state.height_px.saturating_sub(y));
434
435 if w == 0 || h == 0 {
436 continue;
437 }
438
439 draw_calls.push(DrawCall {
440 texture_id: mesh.texture_id,
441 scissor: [x, y, w, h],
442 index_start: index_offset as u32,
443 index_count: mesh.indices.len() as u32,
444 });
445 }
446 Primitive::Callback(_) => {
447 log::warn!("EguiWgpuRenderer: Paint callbacks not supported");
448 }
449 }
450 }
451
452 if draw_calls.is_empty() {
453 return;
454 }
455
456 let vertex_buffer = state
458 .device
459 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
460 label: Some("egui_vertex_buffer"),
461 contents: &vertices,
462 usage: wgpu::BufferUsages::VERTEX,
463 });
464
465 let index_buffer = state
466 .device
467 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
468 label: Some("egui_index_buffer"),
469 contents: bytemuck::cast_slice(&indices),
470 usage: wgpu::BufferUsages::INDEX,
471 });
472
473 let mut render_pass = state
475 .encoder
476 .begin_render_pass(&wgpu::RenderPassDescriptor {
477 label: Some("egui_render_pass"),
478 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
479 view: state.target_view,
480 resolve_target: None,
481 ops: wgpu::Operations {
482 load: wgpu::LoadOp::Load,
483 store: wgpu::StoreOp::Store,
484 },
485 depth_slice: None,
486 })],
487 depth_stencil_attachment: None,
488 timestamp_writes: None,
489 occlusion_query_set: None,
490 multiview_mask: None,
491 });
492
493 render_pass.set_pipeline(pipeline);
494 render_pass.set_bind_group(0, Some(screen_bg), &[]);
495 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
496 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
497
498 for call in &draw_calls {
499 if let Some((_, bind_group)) = self.textures.get(&call.texture_id) {
500 render_pass.set_bind_group(1, Some(bind_group), &[]);
501 render_pass.set_scissor_rect(
502 call.scissor[0],
503 call.scissor[1],
504 call.scissor[2],
505 call.scissor[3],
506 );
507 render_pass.draw_indexed(
508 call.index_start..call.index_start + call.index_count,
509 0,
510 0..1,
511 );
512 } else {
513 log::warn!("EguiWgpuRenderer: Missing texture {:?}", call.texture_id);
514 }
515 }
516 }
517
518 fn set_texture(
521 &mut self,
522 device: &wgpu::Device,
523 queue: &wgpu::Queue,
524 id: TextureId,
525 delta: &ImageDelta,
526 ) {
527 let (width, height) = (delta.image.width() as u32, delta.image.height() as u32);
528
529 let data: Vec<u8> = match &delta.image {
530 ImageData::Color(color_image) => color_image
531 .pixels
532 .iter()
533 .flat_map(|c| c.to_array())
534 .collect(),
535 };
536
537 if let Some(pos) = delta.pos {
538 if let Some((texture, _)) = self.textures.get(&id) {
540 let [x, y] = pos;
541 queue.write_texture(
542 wgpu::TexelCopyTextureInfo {
543 texture,
544 mip_level: 0,
545 origin: wgpu::Origin3d {
546 x: x as u32,
547 y: y as u32,
548 z: 0,
549 },
550 aspect: wgpu::TextureAspect::All,
551 },
552 &data,
553 wgpu::TexelCopyBufferLayout {
554 offset: 0,
555 bytes_per_row: Some(4 * width),
556 rows_per_image: None,
557 },
558 wgpu::Extent3d {
559 width,
560 height,
561 depth_or_array_layers: 1,
562 },
563 );
564 }
565 return;
566 }
567
568 let texture = device.create_texture(&wgpu::TextureDescriptor {
570 label: Some(&format!("egui_texture_{id:?}")),
571 size: wgpu::Extent3d {
572 width,
573 height,
574 depth_or_array_layers: 1,
575 },
576 mip_level_count: 1,
577 sample_count: 1,
578 dimension: wgpu::TextureDimension::D2,
579 format: wgpu::TextureFormat::Rgba8UnormSrgb,
580 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
581 view_formats: &[],
582 });
583
584 queue.write_texture(
585 wgpu::TexelCopyTextureInfo {
586 texture: &texture,
587 mip_level: 0,
588 origin: wgpu::Origin3d::ZERO,
589 aspect: wgpu::TextureAspect::All,
590 },
591 &data,
592 wgpu::TexelCopyBufferLayout {
593 offset: 0,
594 bytes_per_row: Some(4 * width),
595 rows_per_image: None,
596 },
597 wgpu::Extent3d {
598 width,
599 height,
600 depth_or_array_layers: 1,
601 },
602 );
603
604 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
605
606 let (Some(texture_bgl), Some(sampler)) = (
607 self.texture_bind_group_layout.as_ref(),
608 self.sampler.as_ref(),
609 ) else {
610 log::error!(
611 "EguiWgpuRenderer: set_texture called before initialize(); skipping upload"
612 );
613 return;
614 };
615
616 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
617 label: Some(&format!("egui_texture_bg_{id:?}")),
618 layout: texture_bgl,
619 entries: &[
620 wgpu::BindGroupEntry {
621 binding: 0,
622 resource: wgpu::BindingResource::TextureView(&view),
623 },
624 wgpu::BindGroupEntry {
625 binding: 1,
626 resource: wgpu::BindingResource::Sampler(sampler),
627 },
628 ],
629 });
630
631 self.textures.insert(id, (texture, bind_group));
632 }
633}
634
635struct DrawCall {
637 texture_id: TextureId,
638 scissor: [u32; 4], index_start: u32,
640 index_count: u32,
641}