From 74947d27e7d8683b0dedbb3e4b29233dd463b9bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Pakalns?= Date: Sun, 16 Feb 2025 13:46:17 +0200 Subject: [PATCH] Bevy egui performance improvements (#359) * update to egui 0.31 * update examples * Support better parallelization for egui UI rendering * Scissor rect optimization and do not allocate unneeded Vec * Improve EguiRenderOutput extraction performance, fix index buffer bug. * Fix cargo clippy suggestions * Remove redundant logic to round up to power of 2 --------- Co-authored-by: tomara_x <86204514+tomara-x@users.noreply.github.com> --- src/egui_node.rs | 526 ++++++++++++++---------------------------- src/lib.rs | 40 ++-- src/output.rs | 6 +- src/render_systems.rs | 233 ++++++++++++++++++- 4 files changed, 429 insertions(+), 376 deletions(-) diff --git a/src/egui_node.rs b/src/egui_node.rs index 0c87df38..abcdaa4e 100644 --- a/src/egui_node.rs +++ b/src/egui_node.rs @@ -1,8 +1,9 @@ use crate::{ render_systems::{ - EguiPipelines, EguiTextureBindGroups, EguiTextureId, EguiTransform, EguiTransforms, + EguiPipelines, EguiRenderData, EguiTextureBindGroups, EguiTextureId, EguiTransform, + EguiTransforms, }, - EguiContextSettings, EguiRenderOutput, EguiRenderToImage, RenderTargetSize, + EguiRenderToImage, }; use bevy_asset::prelude::*; use bevy_ecs::{ @@ -10,27 +11,25 @@ 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, RenderAssets}, render_graph::{Node, NodeRunError, RenderGraphContext}, render_phase::TrackedRenderPass, render_resource::{ BindGroupLayout, BindGroupLayoutEntry, BindingType, BlendComponent, BlendFactor, - BlendOperation, BlendState, Buffer, BufferAddress, BufferBindingType, BufferDescriptor, - BufferUsages, ColorTargetState, ColorWrites, Extent3d, FragmentState, FrontFace, - IndexFormat, LoadOp, MultisampleState, Operations, PipelineCache, PrimitiveState, - RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, - SamplerBindingType, Shader, ShaderStages, ShaderType, SpecializedRenderPipeline, StoreOp, - TextureDimension, TextureFormat, TextureSampleType, TextureViewDimension, - VertexBufferLayout, VertexFormat, VertexState, VertexStepMode, + BlendOperation, BlendState, BufferBindingType, ColorTargetState, ColorWrites, + CommandEncoderDescriptor, Extent3d, FragmentState, FrontFace, IndexFormat, LoadOp, + MultisampleState, Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment, + RenderPassDescriptor, RenderPipelineDescriptor, SamplerBindingType, Shader, ShaderStages, + ShaderType, SpecializedRenderPipeline, StoreOp, TextureDimension, TextureFormat, + TextureSampleType, TextureViewDimension, VertexBufferLayout, VertexFormat, VertexState, + VertexStepMode, }, - renderer::{RenderContext, RenderDevice, RenderQueue}, + renderer::{RenderContext, RenderDevice}, sync_world::{MainEntity, RenderEntity}, texture::GpuImage, view::{ExtractedWindow, ExtractedWindows}, }; -use bytemuck::cast_slice; use egui::{TextureFilter, TextureOptions}; /// Egui shader. @@ -210,15 +209,6 @@ pub struct EguiNode { render_target_main_entity: MainEntity, render_target_render_entity: RenderEntity, render_target_type: EguiRenderTargetType, - 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 EguiNode { @@ -232,214 +222,38 @@ impl EguiNode { render_target_main_entity, render_target_render_entity, render_target_type, - 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 EguiNode { fn update(&mut self, world: &mut World) { - let mut render_target_query = world.query::<( - &EguiContextSettings, - &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 paint_jobs = std::mem::take(&mut render_output.paint_jobs); - - // 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 - } - }; - - 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 mut index_offset = 0; - - self.draw_commands.clear(); - self.vertex_data.clear(); - self.index_data.clear(); - self.postponed_updates.clear(); - - let render_device = world.resource::(); - - 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 callback = match paint_callback.callback.downcast::() - { - Ok(callback) => callback, - Err(err) => { - log::error!("Unsupported Egui paint callback type: {err:?}"); - continue; - } - }; - - 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_target_main_entity, id) - } - egui::TextureId::User(id) => EguiTextureId::User(id), + world.resource_scope(|world, mut render_data: Mut| { + let Some(data) = render_data.0.get_mut(&self.render_target_main_entity) else { + return; }; - 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() + let (Some(render_target_size), Some(key)) = (data.render_target_size, data.key) else { + bevy_log::warn!("Failed to retrieve egui node data!"); + return; }; - 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_target_render_entity, key, world); - } + for (clip_rect, command) in data.postponed_updates.drain(..) { + let info = egui::PaintCallbackInfo { + viewport: command.rect, + clip_rect, + pixels_per_point: data.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_target_render_entity, key, world); + } + }); } fn run<'w>( @@ -450,6 +264,12 @@ impl Node for EguiNode { ) -> Result<(), NodeRunError> { let egui_pipelines = &world.resource::().0; let pipeline_cache = world.resource::(); + let render_data = world.resource::(); + + let Some(data) = render_data.0.get(&self.render_target_main_entity) else { + bevy_log::warn!("Failed to retrieve render data for egui node rendering!"); + return Ok(()); + }; let (key, swap_chain_texture_view, physical_width, physical_height, load_op) = match self.render_target_type { @@ -498,26 +318,21 @@ impl Node for EguiNode { } }; - let render_queue = world.resource::(); - - let (vertex_buffer, index_buffer) = match (&self.vertex_buffer, &self.index_buffer) { + let (vertex_buffer, index_buffer) = match (&data.vertex_buffer, &data.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 { + for draw_command in &data.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, + pixels_per_point: data.pixels_per_point, screen_size_px: [physical_width, physical_height], }; @@ -532,29 +347,6 @@ impl Node for EguiNode { } } - let bind_groups = &world.resource::().0; - let egui_transforms = world.resource::(); - let device = world.resource::(); - - let render_pass = - render_context - .command_encoder() - .begin_render_pass(&RenderPassDescriptor { - label: Some("egui render pass"), - color_attachments: &[Some(RenderPassColorAttachment { - view: swap_chain_texture_view, - resolve_target: None, - ops: Operations { - load: load_op, - store: StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - let mut render_pass = TrackedRenderPass::new(device, render_pass); - let pipeline_id = egui_pipelines .get(&self.render_target_main_entity) .expect("Expected a queued pipeline"); @@ -562,136 +354,166 @@ impl Node for EguiNode { return Ok(()); }; + let bind_groups = world.resource::(); + let egui_transforms = world.resource::(); 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 render_target_render_entity = self.render_target_render_entity; - 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., - physical_width as f32, - physical_height 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, - }, - }; + render_context.add_command_buffer_generation_task(move |device| { + let mut command_encoder = device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("egui_node_command_encoder"), + }); - let scissor_rect = - clip_urect.intersect(bevy_math::URect::new(0, 0, physical_width, physical_height)); - if scissor_rect.is_empty() { - continue; - } + let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor { + label: Some("egui render pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: swap_chain_texture_view, + resolve_target: None, + ops: Operations { + load: load_op, + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + let mut render_pass = TrackedRenderPass::new(&device, render_pass); + + let mut requires_reset = true; + let mut last_scissor_rect = None; + + let mut vertex_offset: u32 = 0; + for draw_command in &data.draw_commands { + if requires_reset { + render_pass.set_viewport( + 0., + 0., + physical_width as f32, + physical_height as f32, + 0., + 1., + ); + last_scissor_rect = None; + render_pass.set_render_pipeline(pipeline); + render_pass.set_bind_group( + 0, + transform_buffer_bind_group, + &[transform_buffer_offset], + ); - render_pass.set_scissor_rect( - scissor_rect.min.x, - scissor_rect.min.y, - scissor_rect.width(), - scissor_rect.height(), - ); + requires_reset = false; + } - 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; - } - }; + let clip_urect = bevy_math::URect { + min: bevy_math::UVec2 { + x: (draw_command.clip_rect.min.x * data.pixels_per_point).round() as u32, + y: (draw_command.clip_rect.min.y * data.pixels_per_point).round() as u32, + }, + max: bevy_math::UVec2 { + x: (draw_command.clip_rect.max.x * data.pixels_per_point).round() as u32, + y: (draw_command.clip_rect.max.y * data.pixels_per_point).round() as u32, + }, + }; - render_pass.set_bind_group(1, texture_bind_group, &[]); + let scissor_rect = clip_urect.intersect(bevy_math::URect::new( + 0, + 0, + physical_width, + physical_height, + )); + if scissor_rect.is_empty() { + continue; + } - 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() - .expect("Expected an initialized index buffer") - .slice(..), - 0, - IndexFormat::Uint32, - ); + if Some(scissor_rect) != last_scissor_rect { + last_scissor_rect = Some(scissor_rect); - render_pass.draw_indexed( - vertex_offset..(vertex_offset + command.vertices_count as u32), - 0, - 0..1, + // Bevy TrackedRenderPass doesn't track set_scissor_rect calls + // So set_scissor_rect is updated only when it is needed + render_pass.set_scissor_rect( + scissor_rect.min.x, + scissor_rect.min.y, + scissor_rect.width(), + scissor_rect.height(), ); - - 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: [physical_width, physical_height], - }; - 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., + 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, vertex_buffer.slice(..)); + render_pass.set_index_buffer( + index_buffer.slice(..), + 0, + IndexFormat::Uint32, ); - command.callback.cb().render( - info, - &mut render_pass, - self.render_target_render_entity, - key, - world, + 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: data.pixels_per_point, + screen_size_px: [physical_width, physical_height], + }; + + 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, + render_target_render_entity, + key, + world, + ); + } } } } - } + + drop(render_pass); + command_encoder.finish() + }); Ok(()) } } -pub(crate) fn as_color_image(image: egui::ImageData) -> egui::ColorImage { +pub(crate) fn as_color_image(image: &egui::ImageData) -> egui::ColorImage { match image { - egui::ImageData::Color(image) => (*image).clone(), - egui::ImageData::Font(image) => alpha_image_as_color_image(&image), + egui::ImageData::Color(image) => (**image).clone(), + egui::ImageData::Font(image) => alpha_image_as_color_image(image), } } diff --git a/src/lib.rs b/src/lib.rs index b2fd0aad..62e4d71d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -129,7 +129,7 @@ use crate::text_agent::{ #[cfg(feature = "render")] use crate::{ egui_node::{EguiPipeline, EGUI_SHADER_HANDLE}, - render_systems::{EguiTransforms, ExtractedEguiManagedTextures}, + render_systems::{EguiRenderData, EguiTransforms, ExtractedEguiManagedTextures}, }; #[cfg(all( feature = "manage_clipboard", @@ -172,6 +172,7 @@ use output::process_output_system; not(any(target_arch = "wasm32", target_os = "android")) ))] use std::cell::{RefCell, RefMut}; +use std::sync::Arc; #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; @@ -334,11 +335,16 @@ pub struct EguiClipboard { pub struct EguiRenderOutput { /// Pairs of rectangles and paint commands. /// - /// The field gets populated during the [`EguiPostUpdateSet::ProcessOutput`] system (belonging to bevy's [`PostUpdate`]) and reset during [`egui_node::EguiNode`]'s `update`. - pub paint_jobs: Vec, + /// The field gets populated during the [`EguiPostUpdateSet::ProcessOutput`] system (belonging to bevy's [`PostUpdate`]) + /// and processed during [`egui_node::EguiNode`]'s `update`. + /// + /// Value is wrapped in [`Arc`] to improve [`ExtractComponent`] performance. + pub paint_jobs: Arc>, /// The change in egui textures since last frame. - pub textures_delta: egui::TexturesDelta, + /// + /// Value is wrapped in [`Arc`] to improve [`ExtractComponent`] performance. + pub textures_delta: Arc, } impl EguiRenderOutput { @@ -1012,6 +1018,7 @@ impl Plugin for EguiPlugin { .init_resource::() .init_resource::>() .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. @@ -1027,6 +1034,10 @@ impl Plugin for EguiPlugin { Render, render_systems::prepare_egui_transforms_system.in_set(RenderSet::Prepare), ) + .add_systems( + Render, + render_systems::prepare_egui_render_target_data.in_set(RenderSet::Prepare), + ) .add_systems( Render, render_systems::queue_bind_groups_system.in_set(RenderSet::Queue), @@ -1189,20 +1200,18 @@ pub fn capture_pointer_input_system( #[cfg(feature = "render")] pub fn update_egui_textures_system( mut egui_render_output: Query< - (Entity, &mut EguiRenderOutput), + (Entity, &EguiRenderOutput), Or<(With, With)>, >, mut egui_managed_textures: ResMut, mut image_assets: ResMut>, ) { - for (entity, mut egui_render_output) in egui_render_output.iter_mut() { - let set_textures = std::mem::take(&mut egui_render_output.textures_delta.set); - - for (texture_id, image_delta) in set_textures { - let color_image = egui_node::as_color_image(image_delta.image); + for (entity, egui_render_output) in egui_render_output.iter_mut() { + for (texture_id, image_delta) in &egui_render_output.textures_delta.set { + let color_image = egui_node::as_color_image(&image_delta.image); let texture_id = match texture_id { - egui::TextureId::Managed(texture_id) => texture_id, + egui::TextureId::Managed(texture_id) => *texture_id, egui::TextureId::User(_) => continue, }; @@ -1252,17 +1261,16 @@ pub fn update_egui_textures_system( #[cfg(feature = "render")] pub fn free_egui_textures_system( mut egui_user_textures: ResMut, - mut egui_render_output: Query< - (Entity, &mut EguiRenderOutput), + egui_render_output: Query< + (Entity, &EguiRenderOutput), Or<(With, With)>, >, mut egui_managed_textures: ResMut, mut image_assets: ResMut>, mut image_events: EventReader>, ) { - for (entity, mut egui_render_output) in egui_render_output.iter_mut() { - let free_textures = std::mem::take(&mut egui_render_output.textures_delta.free); - for texture_id in free_textures { + for (entity, egui_render_output) in egui_render_output.iter() { + for &texture_id in &egui_render_output.textures_delta.free { if let egui::TextureId::Managed(texture_id) = texture_id { let managed_texture = egui_managed_textures.remove(&(entity, texture_id)); if let Some(managed_texture) = managed_texture { diff --git a/src/output.rs b/src/output.rs index 03309aaf..9b4c80a2 100644 --- a/src/output.rs +++ b/src/output.rs @@ -8,7 +8,7 @@ use bevy_ecs::{ }; use bevy_window::RequestRedraw; use bevy_winit::{cursor::CursorIcon, EventLoopProxy, WakeUp}; -use std::time::Duration; +use std::{sync::Arc, time::Duration}; /// Reads Egui output. pub fn process_output_system( @@ -45,8 +45,8 @@ pub fn process_output_system( } = full_output; let paint_jobs = ctx.tessellate(shapes, pixels_per_point); - render_output.paint_jobs = paint_jobs; - render_output.textures_delta.append(textures_delta); + render_output.paint_jobs = Arc::new(paint_jobs); + render_output.textures_delta = Arc::new(textures_delta); for command in platform_output.commands { match command { diff --git a/src/render_systems.rs b/src/render_systems.rs index c2fbb6c4..f3a08003 100644 --- a/src/render_systems.rs +++ b/src/render_systems.rs @@ -1,7 +1,10 @@ use crate::{ - egui_node::{EguiNode, EguiPipeline, EguiPipelineKey, EguiRenderTargetType}, - EguiContextSettings, EguiManagedTextures, EguiRenderToImage, EguiUserTextures, - RenderTargetSize, + egui_node::{ + DrawCommand, DrawPrimitive, EguiBevyPaintCallback, EguiDraw, EguiNode, EguiPipeline, + EguiPipelineKey, EguiRenderTargetType, PaintCallbackDraw, + }, + EguiContextSettings, EguiManagedTextures, EguiRenderOutput, EguiRenderToImage, + EguiUserTextures, RenderTargetSize, }; use bevy_asset::prelude::*; use bevy_derive::{Deref, DerefMut}; @@ -14,8 +17,8 @@ use bevy_render::{ render_asset::RenderAssets, render_graph::{RenderGraph, RenderLabel}, render_resource::{ - BindGroup, BindGroupEntry, BindingResource, BufferId, CachedRenderPipelineId, - DynamicUniformBuffer, PipelineCache, SpecializedRenderPipelines, + BindGroup, BindGroupEntry, BindingResource, Buffer, BufferDescriptor, BufferId, + CachedRenderPipelineId, DynamicUniformBuffer, PipelineCache, SpecializedRenderPipelines, }, renderer::{RenderDevice, RenderQueue}, sync_world::{MainEntity, RenderEntity}, @@ -25,6 +28,8 @@ use bevy_render::{ }; use bevy_utils::HashMap; use bevy_window::Window; +use bytemuck::cast_slice; +use wgpu_types::{BufferAddress, BufferUsages}; /// Extracted Egui settings. #[derive(Resource, Deref, DerefMut, Default)] @@ -334,3 +339,221 @@ pub fn queue_pipelines_system( commands.insert_resource(EguiPipelines(pipelines)); } + +/// Cached Pipeline IDs for the specialized instances of `EguiPipeline`. +#[derive(Default, Resource)] +pub struct EguiRenderData(pub(crate) HashMap); + +#[derive(Default)] +pub(crate) struct EguiRenderTargetData { + keep: bool, + pub(crate) vertex_data: Vec, + pub(crate) vertex_buffer_capacity: usize, + pub(crate) vertex_buffer: Option, + pub(crate) index_data: Vec, + pub(crate) index_buffer_capacity: usize, + pub(crate) index_buffer: Option, + pub(crate) draw_commands: Vec, + pub(crate) postponed_updates: Vec<(egui::Rect, PaintCallbackDraw)>, + pub(crate) pixels_per_point: f32, + pub(crate) key: Option, + pub(crate) render_target_size: Option, +} + +/// Prepares Egui transforms. +pub fn prepare_egui_render_target_data( + mut render_data: ResMut, + render_targets: Query<( + &MainEntity, + &EguiContextSettings, + &RenderTargetSize, + &EguiRenderOutput, + Option<&EguiRenderToImage>, + )>, + render_device: Res, + render_queue: Res, + extracted_windows: Res, + gpu_images: Res>, +) { + let render_data = &mut render_data.0; + render_data.retain(|_, data| { + let keep = data.keep; + data.keep = false; + keep + }); + + for (main_entity, egui_settings, render_target_size, render_output, render_to_image) in + render_targets.iter() + { + let data = render_data.entry(*main_entity).or_default(); + + data.keep = true; + + 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()); + + data.render_target_size = Some(render_target_size); + + let render_target_type = if render_to_image.is_some() { + EguiRenderTargetType::Image + } else { + EguiRenderTargetType::Window + }; + + // Construct a pipeline key based on a render target. + let key = match render_target_type { + EguiRenderTargetType::Window => { + let Some(key) = extracted_windows + .windows + .get(&main_entity.id()) + .and_then(EguiPipelineKey::from_extracted_window) + else { + continue; + }; + key + } + EguiRenderTargetType::Image => { + let image_handle = image_handle + .expect("Expected an image handle for a render to image node") + .clone(); + let Some(key) = gpu_images + .get(&image_handle) + .map(EguiPipelineKey::from_gpu_image) + else { + continue; + }; + key + } + }; + data.key = Some(key); + + data.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 { + continue; + } + + let mut index_offset = 0; + + data.draw_commands.clear(); + data.vertex_data.clear(); + data.index_data.clear(); + data.postponed_updates.clear(); + + for egui::epaint::ClippedPrimitive { + clip_rect, + primitive, + } in render_output.paint_jobs.as_slice() + { + let clip_rect = *clip_rect; + + let clip_urect = bevy_math::URect { + min: bevy_math::UVec2 { + x: (clip_rect.min.x * data.pixels_per_point).round() as u32, + y: (clip_rect.min.y * data.pixels_per_point).round() as u32, + }, + max: bevy_math::UVec2 { + x: (clip_rect.max.x * data.pixels_per_point).round() as u32, + y: (clip_rect.max.y * data.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 callback = match paint_callback + .callback + .clone() + .downcast::() + { + Ok(callback) => callback, + Err(err) => { + log::error!("Unsupported Egui paint callback type: {err:?}"); + continue; + } + }; + + data.postponed_updates.push(( + clip_rect, + PaintCallbackDraw { + callback: callback.clone(), + rect: paint_callback.rect, + }, + )); + + data.draw_commands.push(DrawCommand { + primitive: DrawPrimitive::PaintCallback(PaintCallbackDraw { + callback, + rect: paint_callback.rect, + }), + clip_rect, + }); + continue; + } + }; + + data.vertex_data + .extend_from_slice(cast_slice::<_, u8>(mesh.vertices.as_slice())); + data.index_data + .extend(mesh.indices.iter().map(|i| i + index_offset)); + index_offset += mesh.vertices.len() as u32; + + let texture_handle = match mesh.texture_id { + egui::TextureId::Managed(id) => EguiTextureId::Managed(*main_entity, id), + egui::TextureId::User(id) => EguiTextureId::User(id), + }; + + data.draw_commands.push(DrawCommand { + primitive: DrawPrimitive::Egui(EguiDraw { + vertices_count: mesh.indices.len(), + egui_texture: texture_handle, + }), + clip_rect, + }); + } + + if data.vertex_data.len() > data.vertex_buffer_capacity { + data.vertex_buffer_capacity = data.vertex_data.len().next_power_of_two(); + data.vertex_buffer = Some(render_device.create_buffer(&BufferDescriptor { + label: Some("egui vertex buffer"), + size: data.vertex_buffer_capacity as BufferAddress, + usage: BufferUsages::COPY_DST | BufferUsages::VERTEX, + mapped_at_creation: false, + })); + } + + let index_data_size = data.index_data.len() * std::mem::size_of::(); + if index_data_size > data.index_buffer_capacity { + data.index_buffer_capacity = index_data_size.next_power_of_two(); + data.index_buffer = Some(render_device.create_buffer(&BufferDescriptor { + label: Some("egui index buffer"), + size: data.index_buffer_capacity as BufferAddress, + usage: BufferUsages::COPY_DST | BufferUsages::INDEX, + mapped_at_creation: false, + })); + } + + let (vertex_buffer, index_buffer) = match (&data.vertex_buffer, &data.index_buffer) { + (Some(vertex), Some(index)) => (vertex, index), + _ => { + continue; + } + }; + + render_queue.write_buffer(vertex_buffer, 0, &data.vertex_data); + render_queue.write_buffer(index_buffer, 0, cast_slice(&data.index_data)); + } +}