bevy_egui/
egui_node.rs

1use crate::{
2    render_systems::{
3        EguiPipelines, EguiRenderData, EguiTextureBindGroups, EguiTextureId, EguiTransform,
4        EguiTransforms,
5    },
6    EguiRenderToImage,
7};
8use bevy_asset::{prelude::*, weak_handle};
9use bevy_ecs::{
10    prelude::*,
11    world::{FromWorld, World},
12};
13use bevy_image::{Image, ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor};
14use bevy_render::{
15    render_asset::{RenderAssetUsages, RenderAssets},
16    render_graph::{Node, NodeRunError, RenderGraphContext},
17    render_phase::TrackedRenderPass,
18    render_resource::{
19        BindGroupLayout, BindGroupLayoutEntry, BindingType, BlendComponent, BlendFactor,
20        BlendOperation, BlendState, BufferBindingType, ColorTargetState, ColorWrites,
21        CommandEncoderDescriptor, Extent3d, FragmentState, FrontFace, IndexFormat, LoadOp,
22        MultisampleState, Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment,
23        RenderPassDescriptor, RenderPipelineDescriptor, SamplerBindingType, Shader, ShaderStages,
24        ShaderType, SpecializedRenderPipeline, StoreOp, TextureDimension, TextureFormat,
25        TextureSampleType, TextureViewDimension, VertexBufferLayout, VertexFormat, VertexState,
26        VertexStepMode,
27    },
28    renderer::{RenderContext, RenderDevice},
29    sync_world::{MainEntity, RenderEntity},
30    texture::GpuImage,
31    view::{ExtractedWindow, ExtractedWindows},
32};
33use egui::{TextureFilter, TextureOptions};
34
35/// Egui shader.
36pub const EGUI_SHADER_HANDLE: Handle<Shader> = weak_handle!("05a4d7a0-4f24-4d7f-b606-3f399074261f");
37
38/// Egui render pipeline.
39#[derive(Resource)]
40pub struct EguiPipeline {
41    /// Transform bind group layout.
42    pub transform_bind_group_layout: BindGroupLayout,
43    /// Texture bind group layout.
44    pub texture_bind_group_layout: BindGroupLayout,
45}
46
47impl FromWorld for EguiPipeline {
48    fn from_world(render_world: &mut World) -> Self {
49        let render_device = render_world.resource::<RenderDevice>();
50
51        let transform_bind_group_layout = render_device.create_bind_group_layout(
52            "egui transform bind group layout",
53            &[BindGroupLayoutEntry {
54                binding: 0,
55                visibility: ShaderStages::VERTEX,
56                ty: BindingType::Buffer {
57                    ty: BufferBindingType::Uniform,
58                    has_dynamic_offset: true,
59                    min_binding_size: Some(EguiTransform::min_size()),
60                },
61                count: None,
62            }],
63        );
64
65        let texture_bind_group_layout = render_device.create_bind_group_layout(
66            "egui texture bind group layout",
67            &[
68                BindGroupLayoutEntry {
69                    binding: 0,
70                    visibility: ShaderStages::FRAGMENT,
71                    ty: BindingType::Texture {
72                        sample_type: TextureSampleType::Float { filterable: true },
73                        view_dimension: TextureViewDimension::D2,
74                        multisampled: false,
75                    },
76                    count: None,
77                },
78                BindGroupLayoutEntry {
79                    binding: 1,
80                    visibility: ShaderStages::FRAGMENT,
81                    ty: BindingType::Sampler(SamplerBindingType::Filtering),
82                    count: None,
83                },
84            ],
85        );
86
87        EguiPipeline {
88            transform_bind_group_layout,
89            texture_bind_group_layout,
90        }
91    }
92}
93
94/// Key for specialized pipeline.
95#[derive(PartialEq, Eq, Hash, Clone, Copy)]
96pub struct EguiPipelineKey {
97    /// Texture format of a window's swap chain to render to.
98    pub texture_format: TextureFormat,
99    /// Render target type (e.g. window, image).
100    pub render_target_type: EguiRenderTargetType,
101}
102
103/// Is used to make a render node aware of a render target type.
104#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
105pub enum EguiRenderTargetType {
106    /// Render to a window.
107    Window,
108    /// Render to an image.
109    Image,
110}
111
112impl EguiPipelineKey {
113    /// Constructs a pipeline key from a window.
114    pub fn from_extracted_window(window: &ExtractedWindow) -> Option<Self> {
115        Some(Self {
116            texture_format: window.swap_chain_texture_format?.add_srgb_suffix(),
117            render_target_type: EguiRenderTargetType::Window,
118        })
119    }
120
121    /// Constructs a pipeline key from a gpu image.
122    pub fn from_gpu_image(image: &GpuImage) -> Self {
123        EguiPipelineKey {
124            texture_format: image.texture_format.add_srgb_suffix(),
125            render_target_type: EguiRenderTargetType::Image,
126        }
127    }
128}
129
130impl SpecializedRenderPipeline for EguiPipeline {
131    type Key = EguiPipelineKey;
132
133    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
134        RenderPipelineDescriptor {
135            label: Some("egui render pipeline".into()),
136            layout: vec![
137                self.transform_bind_group_layout.clone(),
138                self.texture_bind_group_layout.clone(),
139            ],
140            vertex: VertexState {
141                shader: EGUI_SHADER_HANDLE,
142                shader_defs: Vec::new(),
143                entry_point: "vs_main".into(),
144                buffers: vec![VertexBufferLayout::from_vertex_formats(
145                    VertexStepMode::Vertex,
146                    [
147                        VertexFormat::Float32x2, // position
148                        VertexFormat::Float32x2, // UV
149                        VertexFormat::Unorm8x4,  // color (sRGB)
150                    ],
151                )],
152            },
153            fragment: Some(FragmentState {
154                shader: EGUI_SHADER_HANDLE,
155                shader_defs: Vec::new(),
156                entry_point: "fs_main".into(),
157                targets: vec![Some(ColorTargetState {
158                    format: key.texture_format,
159                    blend: Some(BlendState {
160                        color: BlendComponent {
161                            src_factor: BlendFactor::One,
162                            dst_factor: BlendFactor::OneMinusSrcAlpha,
163                            operation: BlendOperation::Add,
164                        },
165                        alpha: BlendComponent {
166                            src_factor: BlendFactor::One,
167                            dst_factor: BlendFactor::OneMinusSrcAlpha,
168                            operation: BlendOperation::Add,
169                        },
170                    }),
171                    write_mask: ColorWrites::ALL,
172                })],
173            }),
174            primitive: PrimitiveState {
175                front_face: FrontFace::Cw,
176                cull_mode: None,
177                ..Default::default()
178            },
179            depth_stencil: None,
180            multisample: MultisampleState::default(),
181            push_constant_ranges: vec![],
182            zero_initialize_workgroup_memory: false,
183        }
184    }
185}
186
187pub(crate) struct DrawCommand {
188    pub(crate) clip_rect: egui::Rect,
189    pub(crate) primitive: DrawPrimitive,
190}
191
192pub(crate) enum DrawPrimitive {
193    Egui(EguiDraw),
194    PaintCallback(PaintCallbackDraw),
195}
196
197pub(crate) struct PaintCallbackDraw {
198    pub(crate) callback: std::sync::Arc<EguiBevyPaintCallback>,
199    pub(crate) rect: egui::Rect,
200}
201
202pub(crate) struct EguiDraw {
203    pub(crate) vertices_count: usize,
204    pub(crate) egui_texture: EguiTextureId,
205}
206
207/// Egui render node.
208pub struct EguiNode {
209    render_target_main_entity: MainEntity,
210    render_target_render_entity: RenderEntity,
211    render_target_type: EguiRenderTargetType,
212}
213
214impl EguiNode {
215    /// Constructs Egui render node.
216    pub fn new(
217        render_target_main_entity: MainEntity,
218        render_target_render_entity: RenderEntity,
219        render_target_type: EguiRenderTargetType,
220    ) -> Self {
221        EguiNode {
222            render_target_main_entity,
223            render_target_render_entity,
224            render_target_type,
225        }
226    }
227}
228
229impl Node for EguiNode {
230    fn update(&mut self, world: &mut World) {
231        world.resource_scope(|world, mut render_data: Mut<EguiRenderData>| {
232            let Some(data) = render_data.0.get_mut(&self.render_target_main_entity) else {
233                return;
234            };
235
236            let (Some(render_target_size), Some(key)) = (data.render_target_size, data.key) else {
237                bevy_log::warn!("Failed to retrieve egui node data!");
238                return;
239            };
240
241            for (clip_rect, command) in data.postponed_updates.drain(..) {
242                let info = egui::PaintCallbackInfo {
243                    viewport: command.rect,
244                    clip_rect,
245                    pixels_per_point: data.pixels_per_point,
246                    screen_size_px: [
247                        render_target_size.physical_width as u32,
248                        render_target_size.physical_height as u32,
249                    ],
250                };
251                command
252                    .callback
253                    .cb()
254                    .update(info, self.render_target_render_entity, key, world);
255            }
256        });
257    }
258
259    fn run<'w>(
260        &self,
261        _graph: &mut RenderGraphContext,
262        render_context: &mut RenderContext<'w>,
263        world: &'w World,
264    ) -> Result<(), NodeRunError> {
265        let egui_pipelines = &world.resource::<EguiPipelines>().0;
266        let pipeline_cache = world.resource::<PipelineCache>();
267        let render_data = world.resource::<EguiRenderData>();
268
269        let Some(data) = render_data.0.get(&self.render_target_main_entity) else {
270            bevy_log::warn!("Failed to retrieve render data for egui node rendering!");
271            return Ok(());
272        };
273
274        let (key, swap_chain_texture_view, physical_width, physical_height, load_op) =
275            match self.render_target_type {
276                EguiRenderTargetType::Window => {
277                    let Some(window) = world
278                        .resource::<ExtractedWindows>()
279                        .windows
280                        .get(&self.render_target_main_entity.id())
281                    else {
282                        return Ok(());
283                    };
284
285                    let Some(swap_chain_texture_view) = &window.swap_chain_texture_view else {
286                        return Ok(());
287                    };
288
289                    let Some(key) = EguiPipelineKey::from_extracted_window(window) else {
290                        return Ok(());
291                    };
292                    (
293                        key,
294                        swap_chain_texture_view,
295                        window.physical_width,
296                        window.physical_height,
297                        LoadOp::Load,
298                    )
299                }
300                EguiRenderTargetType::Image => {
301                    let Some(extracted_render_to_image): Option<&EguiRenderToImage> =
302                        world.get(self.render_target_render_entity.id())
303                    else {
304                        return Ok(());
305                    };
306
307                    let gpu_images = world.resource::<RenderAssets<GpuImage>>();
308                    let Some(gpu_image) = gpu_images.get(&extracted_render_to_image.handle) else {
309                        return Ok(());
310                    };
311                    (
312                        EguiPipelineKey::from_gpu_image(gpu_image),
313                        &gpu_image.texture_view,
314                        gpu_image.size.width,
315                        gpu_image.size.height,
316                        extracted_render_to_image.load_op,
317                    )
318                }
319            };
320
321        let (vertex_buffer, index_buffer) = match (&data.vertex_buffer, &data.index_buffer) {
322            (Some(vertex), Some(index)) => (vertex, index),
323            _ => {
324                return Ok(());
325            }
326        };
327
328        for draw_command in &data.draw_commands {
329            match &draw_command.primitive {
330                DrawPrimitive::Egui(_command) => {}
331                DrawPrimitive::PaintCallback(command) => {
332                    let info = egui::PaintCallbackInfo {
333                        viewport: command.rect,
334                        clip_rect: draw_command.clip_rect,
335                        pixels_per_point: data.pixels_per_point,
336                        screen_size_px: [physical_width, physical_height],
337                    };
338
339                    command.callback.cb().prepare_render(
340                        info,
341                        render_context,
342                        self.render_target_render_entity,
343                        key,
344                        world,
345                    );
346                }
347            }
348        }
349
350        let pipeline_id = egui_pipelines
351            .get(&self.render_target_main_entity)
352            .expect("Expected a queued pipeline");
353        let Some(pipeline) = pipeline_cache.get_render_pipeline(*pipeline_id) else {
354            return Ok(());
355        };
356
357        let bind_groups = world.resource::<EguiTextureBindGroups>();
358        let egui_transforms = world.resource::<EguiTransforms>();
359        let transform_buffer_offset = egui_transforms.offsets[&self.render_target_main_entity];
360        let transform_buffer_bind_group = &egui_transforms
361            .bind_group
362            .as_ref()
363            .expect("Expected a prepared bind group")
364            .1;
365        let render_target_render_entity = self.render_target_render_entity;
366
367        render_context.add_command_buffer_generation_task(move |device| {
368            let mut command_encoder = device.create_command_encoder(&CommandEncoderDescriptor {
369                label: Some("egui_node_command_encoder"),
370            });
371
372            let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
373                label: Some("egui render pass"),
374                color_attachments: &[Some(RenderPassColorAttachment {
375                    view: swap_chain_texture_view,
376                    resolve_target: None,
377                    ops: Operations {
378                        load: load_op,
379                        store: StoreOp::Store,
380                    },
381                })],
382                depth_stencil_attachment: None,
383                timestamp_writes: None,
384                occlusion_query_set: None,
385            });
386            let mut render_pass = TrackedRenderPass::new(&device, render_pass);
387
388            let mut requires_reset = true;
389            let mut last_scissor_rect = None;
390
391            let mut vertex_offset: u32 = 0;
392            for draw_command in &data.draw_commands {
393                if requires_reset {
394                    render_pass.set_viewport(
395                        0.,
396                        0.,
397                        physical_width as f32,
398                        physical_height as f32,
399                        0.,
400                        1.,
401                    );
402                    last_scissor_rect = None;
403                    render_pass.set_render_pipeline(pipeline);
404                    render_pass.set_bind_group(
405                        0,
406                        transform_buffer_bind_group,
407                        &[transform_buffer_offset],
408                    );
409
410                    requires_reset = false;
411                }
412
413                let clip_urect = bevy_math::URect {
414                    min: bevy_math::UVec2 {
415                        x: (draw_command.clip_rect.min.x * data.pixels_per_point).round() as u32,
416                        y: (draw_command.clip_rect.min.y * data.pixels_per_point).round() as u32,
417                    },
418                    max: bevy_math::UVec2 {
419                        x: (draw_command.clip_rect.max.x * data.pixels_per_point).round() as u32,
420                        y: (draw_command.clip_rect.max.y * data.pixels_per_point).round() as u32,
421                    },
422                };
423
424                let scissor_rect = clip_urect.intersect(bevy_math::URect::new(
425                    0,
426                    0,
427                    physical_width,
428                    physical_height,
429                ));
430                if scissor_rect.is_empty() {
431                    continue;
432                }
433
434                if Some(scissor_rect) != last_scissor_rect {
435                    last_scissor_rect = Some(scissor_rect);
436
437                    // Bevy TrackedRenderPass doesn't track set_scissor_rect calls
438                    // So set_scissor_rect is updated only when it is needed
439                    render_pass.set_scissor_rect(
440                        scissor_rect.min.x,
441                        scissor_rect.min.y,
442                        scissor_rect.width(),
443                        scissor_rect.height(),
444                    );
445                }
446
447                match &draw_command.primitive {
448                    DrawPrimitive::Egui(command) => {
449                        let texture_bind_group = match bind_groups.get(&command.egui_texture) {
450                            Some(texture_resource) => texture_resource,
451                            None => {
452                                vertex_offset += command.vertices_count as u32;
453                                continue;
454                            }
455                        };
456
457                        render_pass.set_bind_group(1, texture_bind_group, &[]);
458                        render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
459                        render_pass.set_index_buffer(
460                            index_buffer.slice(..),
461                            0,
462                            IndexFormat::Uint32,
463                        );
464
465                        render_pass.draw_indexed(
466                            vertex_offset..(vertex_offset + command.vertices_count as u32),
467                            0,
468                            0..1,
469                        );
470
471                        vertex_offset += command.vertices_count as u32;
472                    }
473                    DrawPrimitive::PaintCallback(command) => {
474                        let info = egui::PaintCallbackInfo {
475                            viewport: command.rect,
476                            clip_rect: draw_command.clip_rect,
477                            pixels_per_point: data.pixels_per_point,
478                            screen_size_px: [physical_width, physical_height],
479                        };
480
481                        let viewport = info.viewport_in_pixels();
482                        if viewport.width_px > 0 && viewport.height_px > 0 {
483                            requires_reset = true;
484                            render_pass.set_viewport(
485                                viewport.left_px as f32,
486                                viewport.top_px as f32,
487                                viewport.width_px as f32,
488                                viewport.height_px as f32,
489                                0.,
490                                1.,
491                            );
492
493                            command.callback.cb().render(
494                                info,
495                                &mut render_pass,
496                                render_target_render_entity,
497                                key,
498                                world,
499                            );
500                        }
501                    }
502                }
503            }
504
505            drop(render_pass);
506            command_encoder.finish()
507        });
508
509        Ok(())
510    }
511}
512
513pub(crate) fn as_color_image(image: &egui::ImageData) -> egui::ColorImage {
514    match image {
515        egui::ImageData::Color(image) => (**image).clone(),
516        egui::ImageData::Font(image) => alpha_image_as_color_image(image),
517    }
518}
519
520fn alpha_image_as_color_image(image: &egui::FontImage) -> egui::ColorImage {
521    egui::ColorImage {
522        size: image.size,
523        pixels: image.srgba_pixels(None).collect(),
524    }
525}
526
527pub(crate) fn color_image_as_bevy_image(
528    egui_image: &egui::ColorImage,
529    sampler_descriptor: ImageSampler,
530) -> Image {
531    let pixels = egui_image
532        .pixels
533        .iter()
534        // We unmultiply Egui textures to premultiply them later in the fragment shader.
535        // As user textures loaded as Bevy assets are not premultiplied (and there seems to be no
536        // convenient way to convert them to premultiplied ones), we do the this with Egui ones.
537        .flat_map(|color| color.to_srgba_unmultiplied())
538        .collect();
539
540    Image {
541        sampler: sampler_descriptor,
542        ..Image::new(
543            Extent3d {
544                width: egui_image.width() as u32,
545                height: egui_image.height() as u32,
546                depth_or_array_layers: 1,
547            },
548            TextureDimension::D2,
549            pixels,
550            TextureFormat::Rgba8UnormSrgb,
551            RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
552        )
553    }
554}
555
556pub(crate) fn texture_options_as_sampler_descriptor(
557    options: &TextureOptions,
558) -> ImageSamplerDescriptor {
559    fn convert_filter(filter: &TextureFilter) -> ImageFilterMode {
560        match filter {
561            egui::TextureFilter::Nearest => ImageFilterMode::Nearest,
562            egui::TextureFilter::Linear => ImageFilterMode::Linear,
563        }
564    }
565    let address_mode = match options.wrap_mode {
566        egui::TextureWrapMode::ClampToEdge => ImageAddressMode::ClampToEdge,
567        egui::TextureWrapMode::Repeat => ImageAddressMode::Repeat,
568        egui::TextureWrapMode::MirroredRepeat => ImageAddressMode::MirrorRepeat,
569    };
570    ImageSamplerDescriptor {
571        mag_filter: convert_filter(&options.magnification),
572        min_filter: convert_filter(&options.minification),
573        address_mode_u: address_mode,
574        address_mode_v: address_mode,
575        ..Default::default()
576    }
577}
578
579/// Callback to execute custom 'wgpu' rendering inside [`EguiNode`] render graph node.
580///
581/// Rendering can be implemented using for example:
582/// * native wgpu rendering libraries,
583/// * or with [`bevy_render::render_phase`] approach.
584pub struct EguiBevyPaintCallback(Box<dyn EguiBevyPaintCallbackImpl>);
585
586impl EguiBevyPaintCallback {
587    /// Creates a new [`egui::epaint::PaintCallback`] from a callback trait instance.
588    pub fn new_paint_callback<T>(rect: egui::Rect, callback: T) -> egui::epaint::PaintCallback
589    where
590        T: EguiBevyPaintCallbackImpl + 'static,
591    {
592        let callback = Self(Box::new(callback));
593        egui::epaint::PaintCallback {
594            rect,
595            callback: std::sync::Arc::new(callback),
596        }
597    }
598
599    pub(crate) fn cb(&self) -> &dyn EguiBevyPaintCallbackImpl {
600        self.0.as_ref()
601    }
602}
603
604/// Callback that executes custom rendering logic
605pub trait EguiBevyPaintCallbackImpl: Send + Sync {
606    /// Paint callback will be rendered in near future, all data must be finalized for render step
607    fn update(
608        &self,
609        info: egui::PaintCallbackInfo,
610        window_entity: RenderEntity,
611        pipeline_key: EguiPipelineKey,
612        world: &mut World,
613    );
614
615    /// Paint callback call before render step
616    ///
617    ///
618    /// Can be used to implement custom render passes
619    /// or to submit command buffers for execution before egui render pass
620    fn prepare_render<'w>(
621        &self,
622        info: egui::PaintCallbackInfo,
623        render_context: &mut RenderContext<'w>,
624        window_entity: RenderEntity,
625        pipeline_key: EguiPipelineKey,
626        world: &'w World,
627    ) {
628        let _ = (info, render_context, window_entity, pipeline_key, world);
629        // Do nothing by default
630    }
631
632    /// Paint callback render step
633    ///
634    /// Native wgpu RenderPass can be retrieved from [`TrackedRenderPass`] by calling
635    /// [`TrackedRenderPass::wgpu_pass`].
636    fn render<'pass>(
637        &self,
638        info: egui::PaintCallbackInfo,
639        render_pass: &mut TrackedRenderPass<'pass>,
640        window_entity: RenderEntity,
641        pipeline_key: EguiPipelineKey,
642        world: &'pass World,
643    );
644}