diff --git a/Cargo.toml b/Cargo.toml index f71c3457..85263b07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ required-features = ["render"] name = "ui" required-features = ["render"] [[example]] -name = "render_egui_to_texture" +name = "render_egui_to_image" required-features = ["render"] [dependencies] diff --git a/examples/paint_callback.rs b/examples/paint_callback.rs index 8569e21b..0be2dad2 100644 --- a/examples/paint_callback.rs +++ b/examples/paint_callback.rs @@ -14,7 +14,7 @@ use bevy::{ }; use bevy_egui::{ egui_node::{EguiBevyPaintCallback, EguiBevyPaintCallbackImpl, EguiPipelineKey}, - EguiContexts, EguiPlugin, EguiRenderToTextureHandle, + EguiContexts, EguiPlugin, EguiRenderToImage, }; use std::path::Path; use wgpu_types::{Extent3d, TextureUsages}; @@ -25,7 +25,7 @@ fn main() { .add_systems(Startup, setup_worldspace) .add_systems( Update, - (ui_example_system, ui_render_to_texture_example_system), + (ui_example_system, ui_render_to_image_example_system), ) .run(); } @@ -209,22 +209,22 @@ fn setup_worldspace( Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0).mesh())), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::WHITE, - base_color_texture: Some(Handle::clone(&output_texture)), + base_color_texture: Some(output_texture.clone()), alpha_mode: AlphaMode::Blend, // Remove this if you want it to use the world's lighting. unlit: true, ..default() })), )); - commands.spawn(EguiRenderToTextureHandle(output_texture)); + commands.spawn(EguiRenderToImage::new(output_texture.clone_weak())); commands.spawn(( Camera3d::default(), Transform::from_xyz(1.5, 1.5, 1.5).looking_at(Vec3::new(0., 0., 0.), Vec3::Y), )); } -fn ui_render_to_texture_example_system( - mut contexts: Query<&mut bevy_egui::EguiContext, With>, +fn ui_render_to_image_example_system( + mut contexts: Query<&mut bevy_egui::EguiContext, With>, ) { for mut ctx in contexts.iter_mut() { egui::Window::new("Worldspace UI").show(ctx.get_mut(), |ui| { diff --git a/examples/render_egui_to_texture.rs b/examples/render_egui_to_image.rs similarity index 72% rename from examples/render_egui_to_texture.rs rename to examples/render_egui_to_image.rs index dfde0f50..a3e17ad0 100644 --- a/examples/render_egui_to_texture.rs +++ b/examples/render_egui_to_image.rs @@ -1,5 +1,5 @@ use bevy::prelude::*; -use bevy_egui::{EguiContexts, EguiPlugin, EguiRenderToTextureHandle}; +use bevy_egui::{EguiContexts, EguiPlugin, EguiRenderToImage}; use wgpu_types::{Extent3d, TextureUsages}; fn main() { @@ -17,12 +17,10 @@ fn update_screenspace(mut contexts: EguiContexts) { }); } -fn update_worldspace( - mut contexts: Query<&mut bevy_egui::EguiContext, With>, -) { +fn update_worldspace(mut contexts: Query<&mut bevy_egui::EguiContext, With>) { for mut ctx in contexts.iter_mut() { egui::Window::new("Worldspace UI").show(ctx.get_mut(), |ui| { - ui.label("I'm rendering to a texture in worldspace!"); + ui.label("I'm rendering to an image in worldspace!"); }); } } @@ -33,34 +31,34 @@ fn setup_worldspace( mut materials: ResMut>, mut commands: Commands, ) { - let output_texture = images.add({ + let image = images.add({ let size = Extent3d { width: 256, height: 256, depth_or_array_layers: 1, }; - let mut output_texture = Image { + let mut image = Image { // You should use `0` so that the pixels are transparent. data: vec![0; (size.width * size.height * 4) as usize], ..default() }; - output_texture.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT; - output_texture.texture_descriptor.size = size; - output_texture + image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT; + image.texture_descriptor.size = size; + image }); commands.spawn(( Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0).mesh())), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::WHITE, - base_color_texture: Some(Handle::clone(&output_texture)), + base_color_texture: Some(Handle::clone(&image)), alpha_mode: AlphaMode::Blend, // Remove this if you want it to use the world's lighting. unlit: true, ..default() })), )); - commands.spawn(EguiRenderToTextureHandle(output_texture)); + commands.spawn(EguiRenderToImage::new(image)); commands.spawn(( Camera3d::default(), Transform::from_xyz(1.5, 1.5, 1.5).looking_at(Vec3::new(0., 0., 0.), Vec3::Y), diff --git a/src/egui_node.rs b/src/egui_node.rs index 243526a7..1015e1de 100644 --- a/src/egui_node.rs +++ b/src/egui_node.rs @@ -2,7 +2,7 @@ use crate::{ render_systems::{ EguiPipelines, EguiTextureBindGroups, EguiTextureId, EguiTransform, EguiTransforms, }, - EguiRenderOutput, EguiSettings, RenderTargetSize, + EguiRenderOutput, EguiRenderToImage, EguiSettings, RenderTargetSize, }; use bevy_asset::prelude::*; use bevy_ecs::{ @@ -10,8 +10,9 @@ use bevy_ecs::{ world::{FromWorld, World}, }; use bevy_image::{Image, ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor}; +use bevy_log as log; use bevy_render::{ - render_asset::RenderAssetUsages, + render_asset::{RenderAssetUsages, RenderAssets}, render_graph::{Node, NodeRunError, RenderGraphContext}, render_phase::TrackedRenderPass, render_resource::{ @@ -46,7 +47,7 @@ pub struct EguiPipeline { impl FromWorld for EguiPipeline { fn from_world(render_world: &mut World) -> Self { - let render_device = render_world.get_resource::().unwrap(); + let render_device = render_world.resource::(); let transform_bind_group_layout = render_device.create_bind_group_layout( "egui transform bind group layout", @@ -96,6 +97,17 @@ impl FromWorld for EguiPipeline { pub struct EguiPipelineKey { /// Texture format of a window's swap chain to render to. pub texture_format: TextureFormat, + /// Render target type (e.g. window, image). + pub render_target_type: EguiRenderTargetType, +} + +/// Is used to make a render node aware of a render target type. +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub enum EguiRenderTargetType { + /// Render to a window. + Window, + /// Render to an image. + Image, } impl EguiPipelineKey { @@ -103,6 +115,7 @@ impl EguiPipelineKey { pub fn from_extracted_window(window: &ExtractedWindow) -> Option { Some(Self { texture_format: window.swap_chain_texture_format?.add_srgb_suffix(), + render_target_type: EguiRenderTargetType::Window, }) } @@ -110,6 +123,7 @@ impl EguiPipelineKey { pub fn from_gpu_image(image: &GpuImage) -> Self { EguiPipelineKey { texture_format: image.texture_format.add_srgb_suffix(), + render_target_type: EguiRenderTargetType::Image, } } } @@ -193,8 +207,9 @@ pub(crate) struct EguiDraw { /// Egui render node. pub struct EguiNode { - window_entity_main: MainEntity, - window_entity_render: RenderEntity, + render_target_main_entity: MainEntity, + render_target_render_entity: RenderEntity, + render_target_type: EguiRenderTargetType, vertex_data: Vec, vertex_buffer_capacity: usize, vertex_buffer: Option, @@ -208,10 +223,15 @@ pub struct EguiNode { impl EguiNode { /// Constructs Egui render node. - pub fn new(window_entity_main: MainEntity, window_entity_render: RenderEntity) -> Self { + pub fn new( + render_target_main_entity: MainEntity, + render_target_render_entity: RenderEntity, + render_target_type: EguiRenderTargetType, + ) -> Self { EguiNode { - window_entity_main, - window_entity_render, + render_target_main_entity, + render_target_render_entity, + render_target_type, draw_commands: Vec::new(), vertex_data: Vec::new(), vertex_buffer_capacity: 0, @@ -227,27 +247,59 @@ impl EguiNode { impl Node for EguiNode { fn update(&mut self, world: &mut World) { - let Some(key) = world - .get_resource::() - .and_then(|windows| windows.windows.get(&self.window_entity_main.id())) - .and_then(EguiPipelineKey::from_extracted_window) + let mut render_target_query = world.query::<( + &EguiSettings, + &RenderTargetSize, + &mut EguiRenderOutput, + Option<&EguiRenderToImage>, + )>(); + + let Ok((egui_settings, render_target_size, mut render_output, render_to_image)) = + render_target_query.get_mut(world, self.render_target_render_entity.id()) else { + log::error!( + "Failed to update Egui render node for {:?} context: missing components", + self.render_target_main_entity.id() + ); return; }; + let render_target_size = *render_target_size; + let egui_settings = egui_settings.clone(); + let image_handle = + render_to_image.map(|render_to_image| render_to_image.handle.clone_weak()); - let mut render_target_query = - world.query::<(&EguiSettings, &RenderTargetSize, &mut EguiRenderOutput)>(); + let paint_jobs = std::mem::take(&mut render_output.paint_jobs); - let Ok((egui_settings, window_size, mut render_output)) = - render_target_query.get_mut(world, self.window_entity_render.id()) - else { - return; + // Construct a pipeline key based on a render target. + let key = match self.render_target_type { + EguiRenderTargetType::Window => { + let Some(key) = world + .resource::() + .windows + .get(&self.render_target_main_entity.id()) + .and_then(EguiPipelineKey::from_extracted_window) + else { + return; + }; + key + } + EguiRenderTargetType::Image => { + let image_handle = image_handle + .expect("Expected an image handle for a render to image node") + .clone(); + let Some(key) = world + .resource::>() + .get(&image_handle) + .map(EguiPipelineKey::from_gpu_image) + else { + return; + }; + key + } }; - let window_size = *window_size; - let paint_jobs = std::mem::take(&mut render_output.paint_jobs); - self.pixels_per_point = window_size.scale_factor * egui_settings.scale_factor; - if window_size.physical_width == 0.0 || window_size.physical_height == 0.0 { + self.pixels_per_point = render_target_size.scale_factor * egui_settings.scale_factor; + if render_target_size.physical_width == 0.0 || render_target_size.physical_height == 0.0 { return; } @@ -258,7 +310,7 @@ impl Node for EguiNode { self.index_data.clear(); self.postponed_updates.clear(); - let render_device = world.get_resource::().unwrap(); + let render_device = world.resource::(); for egui::epaint::ClippedPrimitive { clip_rect, @@ -280,8 +332,8 @@ impl Node for EguiNode { .intersect(bevy_math::URect::new( 0, 0, - window_size.physical_width as u32, - window_size.physical_height as u32, + render_target_size.physical_width as u32, + render_target_size.physical_height as u32, )) .is_empty() { @@ -291,9 +343,13 @@ impl Node for EguiNode { let mesh = match primitive { egui::epaint::Primitive::Mesh(mesh) => mesh, egui::epaint::Primitive::Callback(paint_callback) => { - let Ok(callback) = paint_callback.callback.downcast::() - else { - unimplemented!("Unsupported egui paint callback type"); + let callback = match paint_callback.callback.downcast::() + { + Ok(callback) => callback, + Err(err) => { + log::error!("Unsupported Egui paint callback type: {err:?}"); + continue; + } }; self.postponed_updates.push(( @@ -327,7 +383,9 @@ impl Node for EguiNode { index_offset += mesh.vertices.len() as u32; let texture_handle = match mesh.texture_id { - egui::TextureId::Managed(id) => EguiTextureId::Managed(self.window_entity_main, id), + egui::TextureId::Managed(id) => { + EguiTextureId::Managed(self.render_target_main_entity, id) + } egui::TextureId::User(id) => EguiTextureId::User(id), }; @@ -373,14 +431,14 @@ impl Node for EguiNode { clip_rect, pixels_per_point: self.pixels_per_point, screen_size_px: [ - window_size.physical_width as u32, - window_size.physical_height as u32, + render_target_size.physical_width as u32, + render_target_size.physical_height as u32, ], }; command .callback .cb() - .update(info, self.window_entity_render, key, world); + .update(info, self.render_target_render_entity, key, world); } } @@ -390,20 +448,57 @@ impl Node for EguiNode { render_context: &mut RenderContext<'w>, world: &'w World, ) -> Result<(), NodeRunError> { - let egui_pipelines = &world.get_resource::().unwrap().0; - let pipeline_cache = world.get_resource::().unwrap(); - - let extracted_windows = &world.get_resource::().unwrap().windows; - let extracted_window = extracted_windows.get(&self.window_entity_main.id()); - let swap_chain_texture_view = - match extracted_window.and_then(|v| v.swap_chain_texture_view.as_ref()) { - None => { - return Ok(()); + let egui_pipelines = &world.resource::().0; + let pipeline_cache = world.resource::(); + + let (key, swap_chain_texture_view, physical_width, physical_height, load_op) = + match self.render_target_type { + EguiRenderTargetType::Window => { + let Some(window) = world + .resource::() + .windows + .get(&self.render_target_main_entity.id()) + else { + return Ok(()); + }; + + let Some(swap_chain_texture_view) = &window.swap_chain_texture_view else { + return Ok(()); + }; + + let Some(key) = EguiPipelineKey::from_extracted_window(window) else { + return Ok(()); + }; + ( + key, + swap_chain_texture_view, + window.physical_width, + window.physical_height, + LoadOp::Load, + ) + } + EguiRenderTargetType::Image => { + let Some(extracted_render_to_image): Option<&EguiRenderToImage> = + world.get(self.render_target_render_entity.id()) + else { + return Ok(()); + }; + + let gpu_images = world.resource::>(); + let Some(gpu_image) = gpu_images.get(&extracted_render_to_image.handle) else { + return Ok(()); + }; + ( + EguiPipelineKey::from_gpu_image(gpu_image), + &gpu_image.texture_view, + gpu_image.size.x, + gpu_image.size.y, + extracted_render_to_image.load_op, + ) } - Some(window) => window, }; - let render_queue = world.get_resource::().unwrap(); + let render_queue = world.resource::(); let (vertex_buffer, index_buffer) = match (&self.vertex_buffer, &self.index_buffer) { (Some(vertex), Some(index)) => (vertex, index), @@ -415,18 +510,6 @@ impl Node for EguiNode { render_queue.write_buffer(vertex_buffer, 0, &self.vertex_data); render_queue.write_buffer(index_buffer, 0, &self.index_data); - let (physical_width, physical_height, pipeline_key) = match extracted_window { - Some(window) => ( - window.physical_width, - window.physical_height, - EguiPipelineKey::from_extracted_window(window), - ), - None => unreachable!(), - }; - let Some(key) = pipeline_key else { - return Ok(()); - }; - for draw_command in &self.draw_commands { match &draw_command.primitive { DrawPrimitive::Egui(_command) => {} @@ -441,7 +524,7 @@ impl Node for EguiNode { command.callback.cb().prepare_render( info, render_context, - self.window_entity_render, + self.render_target_render_entity, key, world, ); @@ -449,11 +532,9 @@ impl Node for EguiNode { } } - let bind_groups = &world.get_resource::().unwrap(); - - let egui_transforms = world.get_resource::().unwrap(); - - let device = world.get_resource::().unwrap(); + let bind_groups = &world.resource::().0; + let egui_transforms = world.resource::(); + let device = world.resource::(); let render_pass = render_context @@ -464,7 +545,7 @@ impl Node for EguiNode { view: swap_chain_texture_view, resolve_target: None, ops: Operations { - load: LoadOp::Load, + load: load_op, store: StoreOp::Store, }, })], @@ -474,13 +555,19 @@ impl Node for EguiNode { }); let mut render_pass = TrackedRenderPass::new(device, render_pass); - let pipeline_id = egui_pipelines.get(&self.window_entity_main).unwrap(); + let pipeline_id = egui_pipelines + .get(&self.render_target_main_entity) + .expect("Expected a queued pipeline"); let Some(pipeline) = pipeline_cache.get_render_pipeline(*pipeline_id) else { return Ok(()); }; - let transform_buffer_offset = egui_transforms.offsets[&self.window_entity_main]; - let transform_buffer_bind_group = &egui_transforms.bind_group.as_ref().unwrap().1; + let transform_buffer_offset = egui_transforms.offsets[&self.render_target_main_entity]; + let transform_buffer_bind_group = &egui_transforms + .bind_group + .as_ref() + .expect("Expected a prepared bind group") + .1; let mut requires_reset = true; @@ -541,10 +628,18 @@ impl Node for EguiNode { render_pass.set_bind_group(1, texture_bind_group, &[]); - render_pass - .set_vertex_buffer(0, self.vertex_buffer.as_ref().unwrap().slice(..)); + render_pass.set_vertex_buffer( + 0, + self.vertex_buffer + .as_ref() + .expect("Expected an initialized vertex buffer") + .slice(..), + ); render_pass.set_index_buffer( - self.index_buffer.as_ref().unwrap().slice(..), + self.index_buffer + .as_ref() + .expect("Expected an initialized index buffer") + .slice(..), 0, IndexFormat::Uint32, ); @@ -580,7 +675,7 @@ impl Node for EguiNode { command.callback.cb().render( info, &mut render_pass, - self.window_entity_render, + self.render_target_render_entity, key, world, ); diff --git a/src/egui_render_to_texture_node.rs b/src/egui_render_to_texture_node.rs deleted file mode 100644 index 6f669bb6..00000000 --- a/src/egui_render_to_texture_node.rs +++ /dev/null @@ -1,468 +0,0 @@ -use crate::{ - egui_node::{ - DrawCommand, DrawPrimitive, EguiBevyPaintCallback, EguiDraw, EguiPipelineKey, - PaintCallbackDraw, - }, - render_systems::{EguiPipelines, EguiTextureBindGroups, EguiTextureId, EguiTransforms}, - EguiRenderOutput, EguiRenderToTextureHandle, EguiSettings, RenderTargetSize, -}; -use bevy_ecs::world::World; -use bevy_render::{ - render_asset::RenderAssets, - render_graph::{Node, NodeRunError, RenderGraphContext, RenderLabel}, - render_phase::TrackedRenderPass, - render_resource::{ - Buffer, BufferAddress, BufferDescriptor, BufferUsages, IndexFormat, LoadOp, Operations, - PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, StoreOp, - }, - renderer::{RenderContext, RenderDevice, RenderQueue}, - sync_world::{MainEntity, RenderEntity}, - texture::GpuImage, -}; -use bytemuck::cast_slice; - -/// [`RenderLabel`] type for the Egui Render to Texture pass. -#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] -pub struct EguiRenderToTexturePass { - /// Index of the window entity. - pub entity_index: u32, - /// Generation of the window entity. - pub entity_generation: u32, -} - -/// Egui render to texture node. -pub struct EguiRenderToTextureNode { - render_to_texture_target_render: RenderEntity, - render_to_texture_target_main: MainEntity, - vertex_data: Vec, - vertex_buffer_capacity: usize, - vertex_buffer: Option, - index_data: Vec, - index_buffer_capacity: usize, - index_buffer: Option, - draw_commands: Vec, - postponed_updates: Vec<(egui::Rect, PaintCallbackDraw)>, - pixels_per_point: f32, -} -impl EguiRenderToTextureNode { - /// Constructs Egui render node. - pub fn new( - render_to_texture_target_render: RenderEntity, - render_to_texture_target_main: MainEntity, - ) -> Self { - EguiRenderToTextureNode { - render_to_texture_target_render, - render_to_texture_target_main, - draw_commands: Vec::new(), - vertex_data: Vec::new(), - vertex_buffer_capacity: 0, - vertex_buffer: None, - index_data: Vec::new(), - index_buffer_capacity: 0, - index_buffer: None, - postponed_updates: Vec::new(), - pixels_per_point: 1., - } - } -} -impl Node for EguiRenderToTextureNode { - fn update(&mut self, world: &mut World) { - let Ok(image_handle) = world - .query::<&EguiRenderToTextureHandle>() - .get(world, self.render_to_texture_target_render.id()) - .map(|handle| handle.0.clone_weak()) - else { - return; - }; - let Some(key) = world - .get_resource::>() - .and_then(|render_assets| render_assets.get(&image_handle)) - .map(EguiPipelineKey::from_gpu_image) - else { - return; - }; - - let mut render_target_query = - world.query::<(&EguiSettings, &RenderTargetSize, &mut EguiRenderOutput)>(); - let Ok((egui_settings, render_target_size, mut render_output)) = - render_target_query.get_mut(world, self.render_to_texture_target_render.id()) - else { - return; - }; - - let render_target_size = *render_target_size; - let paint_jobs = std::mem::take(&mut render_output.paint_jobs); - - self.pixels_per_point = render_target_size.scale_factor * egui_settings.scale_factor; - if render_target_size.physical_width == 0.0 || render_target_size.physical_height == 0.0 { - return; - } - - let render_device = world.get_resource::().unwrap(); - let mut index_offset = 0; - - self.draw_commands.clear(); - self.vertex_data.clear(); - self.index_data.clear(); - self.postponed_updates.clear(); - - for egui::epaint::ClippedPrimitive { - clip_rect, - primitive, - } in paint_jobs - { - let clip_urect = bevy_math::URect { - min: bevy_math::UVec2 { - x: (clip_rect.min.x * self.pixels_per_point).round() as u32, - y: (clip_rect.min.y * self.pixels_per_point).round() as u32, - }, - max: bevy_math::UVec2 { - x: (clip_rect.max.x * self.pixels_per_point).round() as u32, - y: (clip_rect.max.y * self.pixels_per_point).round() as u32, - }, - }; - - if clip_urect - .intersect(bevy_math::URect::new( - 0, - 0, - render_target_size.physical_width as u32, - render_target_size.physical_height as u32, - )) - .is_empty() - { - continue; - } - - let mesh = match primitive { - egui::epaint::Primitive::Mesh(mesh) => mesh, - egui::epaint::Primitive::Callback(paint_callback) => { - let Ok(callback) = paint_callback.callback.downcast::() - else { - unimplemented!("Unsupported egui paint callback type"); - }; - - self.postponed_updates.push(( - clip_rect, - PaintCallbackDraw { - callback: callback.clone(), - rect: paint_callback.rect, - }, - )); - - self.draw_commands.push(DrawCommand { - primitive: DrawPrimitive::PaintCallback(PaintCallbackDraw { - callback, - rect: paint_callback.rect, - }), - clip_rect, - }); - continue; - } - }; - - self.vertex_data - .extend_from_slice(cast_slice::<_, u8>(mesh.vertices.as_slice())); - let indices_with_offset = mesh - .indices - .iter() - .map(|i| i + index_offset) - .collect::>(); - self.index_data - .extend_from_slice(cast_slice(indices_with_offset.as_slice())); - index_offset += mesh.vertices.len() as u32; - - let texture_handle = match mesh.texture_id { - egui::TextureId::Managed(id) => { - EguiTextureId::Managed(self.render_to_texture_target_main, id) - } - egui::TextureId::User(id) => EguiTextureId::User(id), - }; - - self.draw_commands.push(DrawCommand { - primitive: DrawPrimitive::Egui(EguiDraw { - vertices_count: mesh.indices.len(), - egui_texture: texture_handle, - }), - clip_rect, - }); - } - - if self.vertex_data.len() > self.vertex_buffer_capacity { - self.vertex_buffer_capacity = if self.vertex_data.len().is_power_of_two() { - self.vertex_data.len() - } else { - self.vertex_data.len().next_power_of_two() - }; - self.vertex_buffer = Some(render_device.create_buffer(&BufferDescriptor { - label: Some("egui vertex buffer"), - size: self.vertex_buffer_capacity as BufferAddress, - usage: BufferUsages::COPY_DST | BufferUsages::VERTEX, - mapped_at_creation: false, - })); - } - if self.index_data.len() > self.index_buffer_capacity { - self.index_buffer_capacity = if self.index_data.len().is_power_of_two() { - self.index_data.len() - } else { - self.index_data.len().next_power_of_two() - }; - self.index_buffer = Some(render_device.create_buffer(&BufferDescriptor { - label: Some("egui index buffer"), - size: self.index_buffer_capacity as BufferAddress, - usage: BufferUsages::COPY_DST | BufferUsages::INDEX, - mapped_at_creation: false, - })); - } - - for (clip_rect, command) in self.postponed_updates.drain(..) { - let info = egui::PaintCallbackInfo { - viewport: command.rect, - clip_rect, - pixels_per_point: self.pixels_per_point, - screen_size_px: [ - render_target_size.physical_width as u32, - render_target_size.physical_height as u32, - ], - }; - command - .callback - .cb() - .update(info, self.render_to_texture_target_render, key, world); - } - } - - fn run<'w>( - &self, - _graph: &mut RenderGraphContext, - render_context: &mut RenderContext<'w>, - world: &'w World, - ) -> Result<(), NodeRunError> { - let egui_pipelines = &world.get_resource::().unwrap().0; - let pipeline_cache = world.get_resource::().unwrap(); - - let extracted_render_to_texture: Option<&EguiRenderToTextureHandle> = - world.get(self.render_to_texture_target_render.id()); - let Some(render_to_texture_gpu_image) = extracted_render_to_texture else { - return Ok(()); - }; - - let gpu_images = world.get_resource::>().unwrap(); - let gpu_image = gpu_images.get(&render_to_texture_gpu_image.0).unwrap(); - let key = EguiPipelineKey::from_gpu_image(gpu_image); - - let render_queue = world.get_resource::().unwrap(); - - let (vertex_buffer, index_buffer) = match (&self.vertex_buffer, &self.index_buffer) { - (Some(vertex), Some(index)) => (vertex, index), - _ => return Ok(()), - }; - - render_queue.write_buffer(vertex_buffer, 0, &self.vertex_data); - render_queue.write_buffer(index_buffer, 0, &self.index_data); - - for draw_command in &self.draw_commands { - match &draw_command.primitive { - DrawPrimitive::Egui(_command) => {} - DrawPrimitive::PaintCallback(command) => { - let info = egui::PaintCallbackInfo { - viewport: command.rect, - clip_rect: draw_command.clip_rect, - pixels_per_point: self.pixels_per_point, - screen_size_px: [gpu_image.size.x, gpu_image.size.y], - }; - - command.callback.cb().prepare_render( - info, - render_context, - self.render_to_texture_target_render, - key, - world, - ); - } - } - } - - let bind_groups = &world.get_resource::().unwrap(); - - let egui_transforms = world.get_resource::().unwrap(); - - let device = world.get_resource::().unwrap(); - - let render_pass = - render_context - .command_encoder() - .begin_render_pass(&RenderPassDescriptor { - label: Some("egui render to texture render pass"), - color_attachments: &[Some(RenderPassColorAttachment { - view: &gpu_image.texture_view, - resolve_target: None, - ops: Operations { - load: LoadOp::Clear(wgpu_types::Color::TRANSPARENT), - store: StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - let mut render_pass = TrackedRenderPass::new(device, render_pass); - - let Some(pipeline_id) = egui_pipelines.get(&self.render_to_texture_target_main) else { - bevy_log::error!("no egui_pipeline"); - return Ok(()); - }; - let Some(pipeline) = pipeline_cache.get_render_pipeline(*pipeline_id) else { - return Ok(()); - }; - - let transform_buffer_offset = egui_transforms.offsets[&self.render_to_texture_target_main]; - let transform_buffer_bind_group = &egui_transforms.bind_group.as_ref().unwrap().1; - - let mut requires_reset = true; - - let mut vertex_offset: u32 = 0; - for draw_command in &self.draw_commands { - if requires_reset { - render_pass.set_viewport( - 0., - 0., - gpu_image.size.x as f32, - gpu_image.size.y as f32, - 0., - 1., - ); - - render_pass.set_render_pipeline(pipeline); - render_pass.set_bind_group( - 0, - transform_buffer_bind_group, - &[transform_buffer_offset], - ); - - requires_reset = false; - } - - let clip_urect = bevy_math::URect { - min: bevy_math::UVec2 { - x: (draw_command.clip_rect.min.x * self.pixels_per_point).round() as u32, - y: (draw_command.clip_rect.min.y * self.pixels_per_point).round() as u32, - }, - max: bevy_math::UVec2 { - x: (draw_command.clip_rect.max.x * self.pixels_per_point).round() as u32, - y: (draw_command.clip_rect.max.y * self.pixels_per_point).round() as u32, - }, - }; - let scrissor_rect = clip_urect.intersect(bevy_math::URect::from_corners( - bevy_math::UVec2::ZERO, - gpu_image.size, - )); - if scrissor_rect.is_empty() { - continue; - } - - render_pass.set_scissor_rect( - scrissor_rect.min.x, - scrissor_rect.min.y, - scrissor_rect.width(), - scrissor_rect.height(), - ); - - match &draw_command.primitive { - DrawPrimitive::Egui(command) => { - let texture_bind_group = match bind_groups.get(&command.egui_texture) { - Some(texture_resource) => texture_resource, - None => { - vertex_offset += command.vertices_count as u32; - continue; - } - }; - - render_pass.set_bind_group(1, texture_bind_group, &[]); - - render_pass - .set_vertex_buffer(0, self.vertex_buffer.as_ref().unwrap().slice(..)); - render_pass.set_index_buffer( - self.index_buffer.as_ref().unwrap().slice(..), - 0, - IndexFormat::Uint32, - ); - - render_pass.draw_indexed( - vertex_offset..(vertex_offset + command.vertices_count as u32), - 0, - 0..1, - ); - - vertex_offset += command.vertices_count as u32; - } - DrawPrimitive::PaintCallback(command) => { - let info = egui::PaintCallbackInfo { - viewport: command.rect, - clip_rect: draw_command.clip_rect, - pixels_per_point: self.pixels_per_point, - screen_size_px: [gpu_image.size.x, gpu_image.size.y], - }; - - let viewport = info.viewport_in_pixels(); - if viewport.width_px > 0 && viewport.height_px > 0 { - requires_reset = true; - render_pass.set_viewport( - viewport.left_px as f32, - viewport.top_px as f32, - viewport.width_px as f32, - viewport.height_px as f32, - 0., - 1., - ); - - command.callback.cb().render( - info, - &mut render_pass, - self.render_to_texture_target_render, - key, - world, - ); - } - } - } - - // if (draw_command.clip_rect.min.x as u32) < physical_width - // && (draw_command.clip_rect.min.y as u32) < physical_height - // { - // let draw_primitive = match &draw_command.primitive { - // DrawPrimitive::Egui(draw_primitive) => draw_primitive, - // DrawPrimitive::PaintCallback(_) => unimplemented!(), - // }; - // let texture_bind_group = match bind_groups.get(&draw_primitive.egui_texture) { - // Some(texture_resource) => texture_resource, - // None => { - // vertex_offset += draw_primitive.vertices_count as u32; - // continue; - // } - // }; - // - // render_pass.set_bind_group(1, texture_bind_group, &[]); - // - // render_pass.set_scissor_rect( - // draw_command.clip_rect.min.x as u32, - // draw_command.clip_rect.min.y as u32, - // (draw_command.clip_rect.width() as u32) - // .min(physical_width.saturating_sub(draw_command.clip_rect.min.x as u32)), - // (draw_command.clip_rect.height() as u32) - // .min(physical_height.saturating_sub(draw_command.clip_rect.min.y as u32)), - // ); - // - // render_pass.draw_indexed( - // vertex_offset..(vertex_offset + draw_primitive.vertices_count as u32), - // 0, - // 0..1, - // ); - // vertex_offset += draw_primitive.vertices_count as u32; - // } - } - - Ok(()) - } -} diff --git a/src/lib.rs b/src/lib.rs index 028bc30b..49ff7039 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,8 +61,6 @@ compile_error!(include_str!("../static/error_web_sys_unstable_apis.txt")); #[cfg(feature = "render")] pub mod egui_node; /// Egui render node for rendering to a texture. -#[cfg(feature = "render")] -pub mod egui_render_to_texture_node; /// Plugin systems for the render app. #[cfg(feature = "render")] pub mod render_systems; @@ -115,7 +113,7 @@ use bevy_reflect::Reflect; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, extract_resource::{ExtractResource, ExtractResourcePlugin}, - render_resource::SpecializedRenderPipelines, + render_resource::{LoadOp, SpecializedRenderPipelines}, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_window::{PrimaryWindow, SystemCursorIcon, Window}; @@ -360,7 +358,7 @@ impl EguiContext { type EguiContextsFilter = With; #[cfg(feature = "render")] -type EguiContextsFilter = Or<(With, With)>; +type EguiContextsFilter = Or<(With, With)>; #[derive(SystemParam)] /// A helper SystemParam that provides a way to get [`EguiContext`] with less boilerplate and @@ -543,10 +541,31 @@ impl EguiContexts<'_, '_> { } } -/// Contains the texture [`Image`] to render to. +/// Contexts with this component will render UI to a specified image. +/// +/// You can create an entity just with this component, `bevy_egui` will initialize an [`EguiContext`] +/// automatically. #[cfg(feature = "render")] #[derive(Component, Clone, Debug, ExtractComponent)] -pub struct EguiRenderToTextureHandle(pub Handle); +pub struct EguiRenderToImage { + /// A handle of an image to render to. + pub handle: Handle, + /// Customizable [`LoadOp`] for the render node which will be created for this context. + /// + /// You'll likely want [`LoadOp::Clear`], unless you need to draw the UI on top of existing + /// pixels of the image. + pub load_op: LoadOp, +} + +impl EguiRenderToImage { + /// Creates a component from an image handle and sets [`EguiRenderToImage::load_op`] to [`LoadOp::Clear]. + pub fn new(handle: Handle) -> Self { + Self { + handle, + load_op: LoadOp::Clear(wgpu_types::Color::TRANSPARENT), + } + } +} /// A resource for storing `bevy_egui` user textures. #[derive(Clone, bevy_ecs::system::Resource, Default, ExtractResource)] @@ -669,7 +688,7 @@ impl Plugin for EguiPlugin { app.add_plugins(ExtractComponentPlugin::::default()); app.add_plugins(ExtractComponentPlugin::::default()); app.add_plugins(ExtractComponentPlugin::::default()); - app.add_plugins(ExtractComponentPlugin::::default()); + app.add_plugins(ExtractComponentPlugin::::default()); } #[cfg(target_arch = "wasm32")] @@ -692,7 +711,7 @@ impl Plugin for EguiPlugin { ( setup_new_windows_system, #[cfg(feature = "render")] - setup_render_to_texture_handles_system, + setup_render_to_image_handles_system, apply_deferred, update_contexts_system, ) @@ -705,7 +724,7 @@ impl Plugin for EguiPlugin { ( setup_new_windows_system, #[cfg(feature = "render")] - setup_render_to_texture_handles_system, + setup_render_to_image_handles_system, apply_deferred, update_contexts_system, ) @@ -820,10 +839,14 @@ impl Plugin for EguiPlugin { .init_resource::>() .init_resource::() .add_systems( + // Seems to be just the set to add/remove nodes, as it'll run before + // `RenderSet::ExtractCommands` where render nodes get updated. ExtractSchedule, ( - render_systems::setup_new_windows_render_system, - render_systems::setup_new_rtt_render_system, + render_systems::setup_new_window_nodes_system, + render_systems::teardown_window_nodes_system, + render_systems::setup_new_render_to_image_nodes_system, + render_systems::teardown_render_to_image_nodes_system, ), ) .add_systems( @@ -867,9 +890,9 @@ pub struct EguiContextQuery { pub window: Option<&'static mut Window>, /// [`CursorIcon`] component. pub cursor: Option<&'static mut CursorIcon>, - /// [`EguiRenderToTextureHandle`] component, when rendering to a texture. + /// [`EguiRenderToImage`] component, when rendering to a texture. #[cfg(feature = "render")] - pub render_to_texture: Option<&'static mut EguiRenderToTextureHandle>, + pub render_to_image: Option<&'static mut EguiRenderToImage>, } impl EguiContextQueryItem<'_> { @@ -927,15 +950,12 @@ pub fn setup_new_windows_system( /// Adds bevy_egui components to newly created windows. #[cfg(feature = "render")] -pub fn setup_render_to_texture_handles_system( +pub fn setup_render_to_image_handles_system( mut commands: Commands, - new_render_to_texture_targets: Query< - Entity, - (Added, Without), - >, + new_render_to_image_targets: Query, Without)>, ) { - for render_to_texture_target in new_render_to_texture_targets.iter() { - commands.entity(render_to_texture_target).insert(( + for render_to_image_target in new_render_to_image_targets.iter() { + commands.entity(render_to_image_target).insert(( EguiContext::default(), EguiSettings::default(), EguiRenderOutput::default(), @@ -953,7 +973,7 @@ pub fn setup_render_to_texture_handles_system( pub fn update_egui_textures_system( mut egui_render_output: Query< (Entity, &mut EguiRenderOutput), - Or<(With, With)>, + Or<(With, With)>, >, mut egui_managed_textures: ResMut, mut image_assets: ResMut>, @@ -1014,7 +1034,7 @@ fn free_egui_textures_system( mut egui_user_textures: ResMut, mut egui_render_output: Query< (Entity, &mut EguiRenderOutput), - Or<(With, With)>, + Or<(With, With)>, >, mut egui_managed_textures: ResMut, mut image_assets: ResMut>, diff --git a/src/render_systems.rs b/src/render_systems.rs index ce33b7be..28e8d35d 100644 --- a/src/render_systems.rs +++ b/src/render_systems.rs @@ -1,13 +1,12 @@ use crate::{ - egui_node::{EguiNode, EguiPipeline, EguiPipelineKey}, - egui_render_to_texture_node::{EguiRenderToTextureNode, EguiRenderToTexturePass}, - EguiManagedTextures, EguiRenderToTextureHandle, EguiSettings, EguiUserTextures, - RenderTargetSize, + egui_node::{EguiNode, EguiPipeline, EguiPipelineKey, EguiRenderTargetType}, + EguiManagedTextures, EguiRenderToImage, EguiSettings, EguiUserTextures, RenderTargetSize, }; use bevy_asset::prelude::*; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, system::SystemParam}; use bevy_image::Image; +use bevy_log as log; use bevy_math::Vec2; use bevy_render::{ extract_resource::ExtractResource, @@ -66,6 +65,28 @@ pub struct EguiPass { pub entity_index: u32, /// Generation of the window entity. pub entity_generation: u32, + /// Render target type (e.g. window, image). + pub render_target_type: EguiRenderTargetType, +} + +impl EguiPass { + /// Creates a pass from a window Egui context. + pub fn from_window_entity(entity: Entity) -> Self { + Self { + entity_index: entity.index(), + entity_generation: entity.generation(), + render_target_type: EguiRenderTargetType::Window, + } + } + + /// Creates a pass from a "render to image" Egui context. + pub fn from_render_to_image_entity(entity: Entity) -> Self { + Self { + entity_index: entity.index(), + entity_generation: entity.generation(), + render_target_type: EguiRenderTargetType::Image, + } + } } impl ExtractedEguiTextures<'_> { @@ -89,44 +110,68 @@ impl ExtractedEguiTextures<'_> { } } -/// Sets up the pipeline for newly created windows. -pub fn setup_new_windows_render_system( +/// Sets up render nodes for newly created window Egui contexts. +pub fn setup_new_window_nodes_system( windows: Extract>>, mut render_graph: ResMut, ) { - for (window, render_window) in windows.iter() { - let egui_pass = EguiPass { - entity_index: render_window.index(), - entity_generation: render_window.generation(), - }; - let new_node = EguiNode::new(MainEntity::from(window), *render_window); + for (window_entity, window_render_entity) in windows.iter() { + let egui_pass = EguiPass::from_window_entity(window_entity); + let new_node = EguiNode::new( + MainEntity::from(window_entity), + *window_render_entity, + EguiRenderTargetType::Window, + ); render_graph.add_node(egui_pass.clone(), new_node); render_graph.add_node_edge(bevy_render::graph::CameraDriverLabel, egui_pass); } } -/// Sets up the pipeline for newly created Render to texture entities. -pub fn setup_new_rtt_render_system( - render_to_texture_targets: Extract< - Query<(Entity, &RenderEntity), Added>, - >, + +/// Tears render nodes down for deleted window Egui contexts. +pub fn teardown_window_nodes_system( + mut removed_windows: Extract>, mut render_graph: ResMut, ) { - for (render_to_texture_target, render_entity) in render_to_texture_targets.iter() { - let egui_rtt_pass = EguiRenderToTexturePass { - entity_index: render_to_texture_target.index(), - entity_generation: render_to_texture_target.generation(), - }; + for window_entity in removed_windows.read() { + if let Err(err) = render_graph.remove_node(EguiPass::from_window_entity(window_entity)) { + log::error!("Failed to remove a render graph node: {err:?}"); + } + } +} + +/// Sets up render nodes for newly created "render to texture" Egui contexts. +pub fn setup_new_render_to_image_nodes_system( + render_to_image_targets: Extract>>, + mut render_graph: ResMut, +) { + for (render_to_image_entity, render_entity) in render_to_image_targets.iter() { + let egui_pass = EguiPass::from_render_to_image_entity(render_to_image_entity); - let new_node = EguiRenderToTextureNode::new( + let new_node = EguiNode::new( + MainEntity::from(render_to_image_entity), *render_entity, - MainEntity::from(render_to_texture_target), + EguiRenderTargetType::Image, ); - render_graph.add_node(egui_rtt_pass.clone(), new_node); + render_graph.add_node(egui_pass.clone(), new_node); + + render_graph.add_node_edge(egui_pass, bevy_render::graph::CameraDriverLabel); + } +} - render_graph.add_node_edge(egui_rtt_pass, bevy_render::graph::CameraDriverLabel); +/// Tears render nodes down for deleted "render to texture" Egui contexts. +pub fn teardown_render_to_image_nodes_system( + mut removed_windows: Extract>, + mut render_graph: ResMut, +) { + for window_entity in removed_windows.read() { + if let Err(err) = + render_graph.remove_node(EguiPass::from_render_to_image_entity(window_entity)) + { + log::error!("Failed to remove a render graph node: {err:?}"); + } } } @@ -260,7 +305,7 @@ pub fn queue_pipelines_system( mut specialized_pipelines: ResMut>, egui_pipeline: Res, windows: Res, - render_to_texture: Query<(&MainEntity, &EguiRenderToTextureHandle)>, + render_to_image: Query<(&MainEntity, &EguiRenderToImage)>, images: Res>, ) { let mut pipelines: HashMap = windows @@ -274,10 +319,10 @@ pub fn queue_pipelines_system( .collect(); pipelines.extend( - render_to_texture + render_to_image .iter() - .filter_map(|(main_entity, handle)| { - let img = images.get(&handle.0)?; + .filter_map(|(main_entity, render_to_image)| { + let img = images.get(&render_to_image.handle)?; let key = EguiPipelineKey::from_gpu_image(img); let pipeline_id = specialized_pipelines.specialize(&pipeline_cache, &egui_pipeline, key); diff --git a/src/systems.rs b/src/systems.rs index 9abe1c57..91601105 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -1,7 +1,7 @@ #[cfg(target_arch = "wasm32")] use crate::text_agent::{is_mobile_safari, update_text_agent}; #[cfg(feature = "render")] -use crate::EguiRenderToTextureHandle; +use crate::EguiRenderToImage; use crate::{ EguiContext, EguiContextQuery, EguiContextQueryItem, EguiFullOutput, EguiInput, EguiSettings, RenderTargetSize, @@ -526,7 +526,7 @@ pub fn update_contexts_system( )); } #[cfg(feature = "render")] - if let Some(EguiRenderToTextureHandle(handle)) = context.render_to_texture.as_deref() { + if let Some(EguiRenderToImage { handle, .. }) = context.render_to_image.as_deref() { let image = images.get(handle).expect("rtt handle should be valid"); let size = image.size_f32(); render_target_size = Some(RenderTargetSize {