1pub use render_pass::*;
2
3pub mod graph {
5 use bevy_render::render_graph::{RenderLabel, RenderSubGraph};
6
7 #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderSubGraph)]
9 pub struct SubGraphEgui;
10
11 #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
13 pub enum NodeEgui {
14 EguiPass,
16 }
17}
18
19use crate::{
20 EguiContextSettings, EguiRenderOutput, RenderComputedScaleFactor,
21 render::graph::{NodeEgui, SubGraphEgui},
22};
23use bevy_app::SubApp;
24use bevy_asset::{Handle, RenderAssetUsages, uuid_handle};
25use bevy_camera::Camera;
26use bevy_ecs::{
27 component::Component,
28 entity::Entity,
29 query::Has,
30 resource::Resource,
31 system::{Commands, Local, ResMut},
32 world::{FromWorld, World},
33};
34use bevy_image::{
35 BevyDefault, Image, ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor,
36};
37use bevy_math::{Mat4, UVec4};
38use bevy_mesh::VertexBufferLayout;
39use bevy_platform::collections::HashSet;
40use bevy_render::{
41 MainWorld,
42 render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext},
43 render_phase::TrackedRenderPass,
44 render_resource::{
45 BindGroupLayout, BindGroupLayoutEntries, FragmentState, RenderPipelineDescriptor,
46 SpecializedRenderPipeline, VertexState,
47 binding_types::{sampler, texture_2d, uniform_buffer},
48 },
49 renderer::{RenderContext, RenderDevice},
50 sync_world::{RenderEntity, TemporaryRenderEntity},
51 view::{ExtractedView, Hdr, RetainedViewEntity, ViewTarget},
52};
53use bevy_shader::{Shader, ShaderDefVal};
54use egui::{TextureFilter, TextureOptions};
55use std::num::NonZero;
56use systems::{EguiTextureId, EguiTransform};
57use wgpu_types::{
58 BlendState, ColorTargetState, ColorWrites, Extent3d, MultisampleState, PrimitiveState,
59 PushConstantRange, SamplerBindingType, ShaderStages, TextureDimension, TextureFormat,
60 TextureSampleType, VertexFormat, VertexStepMode,
61};
62
63mod render_pass;
64#[cfg(feature = "render")]
66pub mod systems;
67
68#[derive(Component, Debug)]
76pub struct EguiCameraView(pub Entity);
77
78#[derive(Component, Debug)]
86pub struct EguiViewTarget(pub Entity);
87
88pub fn get_egui_graph(render_app: &mut SubApp) -> RenderGraph {
90 let pass_node = EguiPassNode::new(render_app.world_mut());
91 let mut graph = RenderGraph::default();
92 graph.add_node(NodeEgui::EguiPass, pass_node);
93 graph
94}
95
96pub struct RunEguiSubgraphOnEguiViewNode;
98
99impl Node for RunEguiSubgraphOnEguiViewNode {
100 fn run<'w>(
101 &self,
102 graph: &mut RenderGraphContext,
103 _: &mut RenderContext<'w>,
104 world: &'w World,
105 ) -> Result<(), NodeRunError> {
106 let Some(mut render_views) = world.try_query::<&EguiCameraView>() else {
108 return Ok(());
109 };
110 let Ok(default_camera_view) = render_views.get(world, graph.view_entity()) else {
111 return Ok(());
112 };
113
114 graph.run_sub_graph(SubGraphEgui, vec![], Some(default_camera_view.0))?;
116 Ok(())
117 }
118}
119
120pub fn extract_egui_camera_view_system(
122 mut commands: Commands,
123 mut world: ResMut<MainWorld>,
124 mut live_entities: Local<HashSet<RetainedViewEntity>>,
125) {
126 live_entities.clear();
127 let mut q = world.query::<(
128 Entity,
129 RenderEntity,
130 &Camera,
131 &mut EguiRenderOutput,
132 &EguiContextSettings,
133 Has<Hdr>,
134 )>();
135
136 for (main_entity, render_entity, camera, mut egui_render_output, settings, hdr) in
137 &mut q.iter_mut(&mut world)
138 {
139 let egui_render_output = std::mem::take(egui_render_output.as_mut());
141
142 if !camera.is_active {
144 commands
145 .get_entity(render_entity)
146 .expect("Camera entity wasn't synced.")
147 .remove::<EguiCameraView>();
148 continue;
149 }
150
151 const UI_CAMERA_FAR: f32 = 1000.0;
152 const EGUI_CAMERA_SUBVIEW: u32 = 2095931312;
153 const UI_CAMERA_TRANSFORM_OFFSET: f32 = -0.1;
154
155 if let Some(physical_viewport_rect) = camera.physical_viewport_rect() {
156 let projection_matrix = Mat4::orthographic_rh(
158 0.0,
159 physical_viewport_rect.width() as f32,
160 physical_viewport_rect.height() as f32,
161 0.0,
162 0.0,
163 UI_CAMERA_FAR,
164 );
165 let retained_view_entity =
168 RetainedViewEntity::new(main_entity.into(), None, EGUI_CAMERA_SUBVIEW);
169 let ui_camera_view = commands
171 .spawn((
172 ExtractedView {
173 retained_view_entity,
174 clip_from_view: projection_matrix,
175 world_from_view: bevy_transform::components::GlobalTransform::from_xyz(
176 0.0,
177 0.0,
178 UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET,
179 ),
180 clip_from_world: None,
181 hdr,
182 viewport: UVec4::from((
183 physical_viewport_rect.min,
184 physical_viewport_rect.size(),
185 )),
186 color_grading: Default::default(),
187 },
188 EguiViewTarget(render_entity),
190 egui_render_output,
191 RenderComputedScaleFactor {
192 scale_factor: settings.scale_factor
193 * camera.target_scaling_factor().unwrap_or(1.0),
194 },
195 TemporaryRenderEntity,
196 ))
197 .id();
198
199 let mut entity_commands = commands
200 .get_entity(render_entity)
201 .expect("Camera entity wasn't synced.");
202 entity_commands.insert(EguiCameraView(ui_camera_view));
204 live_entities.insert(retained_view_entity);
205 }
206 }
207}
208
209pub const EGUI_SHADER_HANDLE: Handle<Shader> = uuid_handle!("05a4d7a0-4f24-4d7f-b606-3f399074261f");
211
212#[derive(Resource)]
214pub struct EguiRenderSettings {
215 pub bindless_mode_array_size: Option<NonZero<u32>>,
217}
218
219#[derive(Resource)]
221pub struct EguiPipeline {
222 pub transform_bind_group_layout: BindGroupLayout,
224 pub texture_bind_group_layout: BindGroupLayout,
226 pub bindless: Option<NonZero<u32>>,
229}
230
231impl FromWorld for EguiPipeline {
232 fn from_world(render_world: &mut World) -> Self {
233 let render_device = render_world.resource::<RenderDevice>();
234 let settings = render_world.resource::<EguiRenderSettings>();
235
236 let features = render_device.features();
237
238 let bindless = if features.contains(wgpu_types::Features::TEXTURE_BINDING_ARRAY)
243 && features.contains(wgpu_types::Features::PUSH_CONSTANTS)
244 {
245 settings.bindless_mode_array_size
246 } else {
247 None
248 };
249
250 let transform_bind_group_layout = render_device.create_bind_group_layout(
251 "egui_transform_layout",
252 &BindGroupLayoutEntries::single(
253 ShaderStages::VERTEX,
254 uniform_buffer::<EguiTransform>(true),
255 ),
256 );
257
258 let texture_bind_group_layout = if let Some(bindless) = bindless {
259 render_device.create_bind_group_layout(
260 "egui_texture_layout",
261 &BindGroupLayoutEntries::sequential(
262 ShaderStages::FRAGMENT,
263 (
264 texture_2d(TextureSampleType::Float { filterable: true }).count(bindless),
265 sampler(SamplerBindingType::Filtering).count(bindless),
266 ),
267 ),
268 )
269 } else {
270 render_device.create_bind_group_layout(
271 "egui_texture_layout",
272 &BindGroupLayoutEntries::sequential(
273 ShaderStages::FRAGMENT,
274 (
275 texture_2d(TextureSampleType::Float { filterable: true }),
276 sampler(SamplerBindingType::Filtering),
277 ),
278 ),
279 )
280 };
281
282 EguiPipeline {
283 transform_bind_group_layout,
284 texture_bind_group_layout,
285 bindless,
286 }
287 }
288}
289
290#[derive(PartialEq, Eq, Hash, Clone, Copy)]
292pub struct EguiPipelineKey {
293 pub hdr: bool,
295}
296
297impl SpecializedRenderPipeline for EguiPipeline {
298 type Key = EguiPipelineKey;
299
300 fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
301 let mut shader_defs = Vec::new();
302 let mut push_constant_ranges = Vec::new();
303
304 if let Some(bindless) = self.bindless {
305 shader_defs.push(ShaderDefVal::UInt("BINDLESS".into(), u32::from(bindless)));
306 push_constant_ranges.push(PushConstantRange {
307 stages: ShaderStages::FRAGMENT,
308 range: 0..4,
309 });
310 }
311
312 RenderPipelineDescriptor {
313 label: Some("egui_pipeline".into()),
314 layout: vec![
315 self.transform_bind_group_layout.clone(),
316 self.texture_bind_group_layout.clone(),
317 ],
318 vertex: VertexState {
319 shader: EGUI_SHADER_HANDLE,
320 shader_defs: shader_defs.clone(),
321 entry_point: Some("vs_main".into()),
322 buffers: vec![VertexBufferLayout::from_vertex_formats(
323 VertexStepMode::Vertex,
324 [
325 VertexFormat::Float32x2, VertexFormat::Float32x2, VertexFormat::Unorm8x4, ],
329 )],
330 },
331 fragment: Some(FragmentState {
332 shader: EGUI_SHADER_HANDLE,
333 shader_defs,
334 entry_point: Some("fs_main".into()),
335 targets: vec![Some(ColorTargetState {
336 format: if key.hdr {
337 ViewTarget::TEXTURE_FORMAT_HDR
338 } else {
339 TextureFormat::bevy_default()
340 },
341 blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
342 write_mask: ColorWrites::ALL,
343 })],
344 }),
345 primitive: PrimitiveState::default(),
346 depth_stencil: None,
347 multisample: MultisampleState::default(),
348 push_constant_ranges,
349 zero_initialize_workgroup_memory: false,
350 }
351 }
352}
353
354pub(crate) struct DrawCommand {
355 pub(crate) clip_rect: egui::Rect,
356 pub(crate) primitive: DrawPrimitive,
357}
358
359pub(crate) enum DrawPrimitive {
360 Egui(EguiDraw),
361 PaintCallback(PaintCallbackDraw),
362}
363
364pub(crate) struct PaintCallbackDraw {
365 pub(crate) callback: std::sync::Arc<EguiBevyPaintCallback>,
366 pub(crate) rect: egui::Rect,
367}
368
369pub(crate) struct EguiDraw {
370 pub(crate) vertices_count: usize,
371 pub(crate) egui_texture: EguiTextureId,
372}
373
374pub(crate) fn as_color_image(image: &egui::ImageData) -> egui::ColorImage {
375 match image {
376 egui::ImageData::Color(image) => (**image).clone(),
377 }
378}
379
380pub(crate) fn color_image_as_bevy_image(
381 egui_image: &egui::ColorImage,
382 sampler_descriptor: ImageSampler,
383) -> Image {
384 let pixels = egui_image
385 .pixels
386 .iter()
387 .flat_map(|color| color.to_srgba_unmultiplied())
391 .collect();
392
393 Image {
394 sampler: sampler_descriptor,
395 ..Image::new(
396 Extent3d {
397 width: egui_image.width() as u32,
398 height: egui_image.height() as u32,
399 depth_or_array_layers: 1,
400 },
401 TextureDimension::D2,
402 pixels,
403 TextureFormat::Rgba8UnormSrgb,
404 RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
405 )
406 }
407}
408
409pub(crate) fn texture_options_as_sampler_descriptor(
410 options: &TextureOptions,
411) -> ImageSamplerDescriptor {
412 fn convert_filter(filter: &TextureFilter) -> ImageFilterMode {
413 match filter {
414 egui::TextureFilter::Nearest => ImageFilterMode::Nearest,
415 egui::TextureFilter::Linear => ImageFilterMode::Linear,
416 }
417 }
418 let address_mode = match options.wrap_mode {
419 egui::TextureWrapMode::ClampToEdge => ImageAddressMode::ClampToEdge,
420 egui::TextureWrapMode::Repeat => ImageAddressMode::Repeat,
421 egui::TextureWrapMode::MirroredRepeat => ImageAddressMode::MirrorRepeat,
422 };
423 ImageSamplerDescriptor {
424 mag_filter: convert_filter(&options.magnification),
425 min_filter: convert_filter(&options.minification),
426 address_mode_u: address_mode,
427 address_mode_v: address_mode,
428 ..Default::default()
429 }
430}
431
432pub struct EguiBevyPaintCallback(Box<dyn EguiBevyPaintCallbackImpl>);
438
439impl EguiBevyPaintCallback {
440 pub fn new_paint_callback<T>(rect: egui::Rect, callback: T) -> egui::epaint::PaintCallback
442 where
443 T: EguiBevyPaintCallbackImpl + 'static,
444 {
445 let callback = Self(Box::new(callback));
446 egui::epaint::PaintCallback {
447 rect,
448 callback: std::sync::Arc::new(callback),
449 }
450 }
451
452 pub(crate) fn cb(&self) -> &dyn EguiBevyPaintCallbackImpl {
453 self.0.as_ref()
454 }
455}
456
457pub trait EguiBevyPaintCallbackImpl: Send + Sync {
459 fn update(
461 &self,
462 info: egui::PaintCallbackInfo,
463 render_entity: RenderEntity,
464 pipeline_key: EguiPipelineKey,
465 world: &mut World,
466 );
467
468 fn prepare_render<'w>(
474 &self,
475 info: egui::PaintCallbackInfo,
476 render_context: &mut RenderContext<'w>,
477 render_entity: RenderEntity,
478 pipeline_key: EguiPipelineKey,
479 world: &'w World,
480 ) {
481 let _ = (info, render_context, render_entity, pipeline_key, world);
482 }
484
485 fn render<'pass>(
490 &self,
491 info: egui::PaintCallbackInfo,
492 render_pass: &mut TrackedRenderPass<'pass>,
493 render_entity: RenderEntity,
494 pipeline_key: EguiPipelineKey,
495 world: &'pass World,
496 );
497}