use std::{ collections::{BTreeMap, HashMap}, mem::ManuallyDrop, sync::Arc, }; use ash::vk; use gpu_allocator::MemoryLocation; use indexmap::IndexMap; use crate::{ EguiState, buffers::{Buffer, BufferDesc}, commands::traits::CommandBufferExt, device::{self, AllocationStrategy}, images::{Image, ImageDesc, ImageViewDesc}, render_graph::{ Access, Barrier, GraphResourceDesc, GraphResourceId, PassDesc, RecordFn, RenderContext, RenderGraph, buffer_barrier, image_barrier, }, texture::{self, TextureId}, }; pub fn egui_pre_pass( dev: &device::Device, rg: &mut RenderGraph, textures: &mut crate::texture::TextureManager, egui_state: &mut EguiState, output: &egui::FullOutput, ) -> crate::Result<()> { // allocate resource ids for textures in tessellated list (imported from texture manager) // define accesses for resource ids if output.textures_delta.set.is_empty() { return Ok(()); } use rectangle_pack::{ GroupedRectsToPlace, RectToInsert, TargetBin, contains_smallest_box, pack_rects, volume_heuristic, }; let mut rects_to_place = GroupedRectsToPlace::::new(); let mut max_width = 0; let mut max_height = 0; // create textures for new egui textures // these are real images that will be sampled from in the shader. let updates = output .textures_delta .set .iter() .map(|(egui_id, delta)| { max_width = max_width.max(delta.image.width() as u32); max_height = max_height.max(delta.image.height() as u32); use std::collections::hash_map::Entry; let id = match egui_state.textures.entry(*egui_id) { Entry::Occupied(entry) => entry.get().id, Entry::Vacant(entry) => { tracing::trace!("creating texture image for egui image {egui_id:?}"); let Ok(image) = Image::new( dev.clone(), ImageDesc { name: Some(format!("egui-texture-{egui_id:?}").into()), format: vk::Format::R8G8B8A8_UNORM, extent: vk::Extent3D { width: delta.image.width() as u32, height: delta.image.height() as u32, depth: 1, }, usage: vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST, mem_location: MemoryLocation::GpuOnly, alloc_scheme: AllocationStrategy::Dedicated, ..Default::default() }, ) else { return Err(crate::Error::Todo( "handle image creation failure in egui pre-pass", )); }; let id = textures.insert_image(Arc::new(image)); entry.insert(crate::EguiTextureInfo { id, options: delta.options, }); id } }; rects_to_place.push_rect( id, None, RectToInsert::new(delta.image.width() as u32, delta.image.height() as u32, 1), ); Ok((id, (delta.pos, &delta.image))) }) .collect::>>()?; let rect_placements = loop { let mut target_bins = BTreeMap::new(); target_bins.insert(0, TargetBin::new(max_width, max_height, 1)); match pack_rects( &rects_to_place, &mut target_bins, &volume_heuristic, &contains_smallest_box, ) { Ok(placements) => break placements, Err(_) => { max_width *= 2; max_height *= 2; } } }; let extent = rect_placements .packed_locations() .iter() .fold((0u32, 0u32), |(w, h), (_id, (_bin, loc))| { (w.max(loc.x() + loc.width()), h.max(loc.y() + loc.height())) }); tracing::trace!( "creating staging buffer {}x{} for uploading egui textures", extent.0, extent.1, ); let mut staging_buffer = Buffer::new( dev.clone(), BufferDesc { name: Some("egui-prepass-staging-buffer".into()), size: (extent.0 * extent.1 * 4) as u64, // RGBA8 usage: vk::BufferUsageFlags::TRANSFER_SRC, queue_families: device::QueueFlags::empty(), mem_location: MemoryLocation::CpuToGpu, ..Default::default() }, )?; let staging_image = Image::new( dev.clone(), ImageDesc { name: Some("egui-prepass-staging-buffer".into()), format: vk::Format::R8G8B8A8_UNORM, extent: vk::Extent3D { width: extent.0, height: extent.1, depth: 1, }, usage: vk::ImageUsageFlags::TRANSFER_SRC | vk::ImageUsageFlags::TRANSFER_DST, queue_families: device::QueueFlags::empty(), mem_location: MemoryLocation::GpuOnly, ..Default::default() }, )?; let staging_map = staging_buffer.map_mut().unwrap(); struct ImageUpdate { #[allow(dead_code)] id: TextureId, graph_id: GraphResourceId, dest_offset: (u32, u32), src_offset: (u32, u32), size: (u32, u32), } let updates = updates .into_iter() .filter_map(|(id, (pos, image))| { let (_, loc) = rect_placements.packed_locations().get(&id)?; // copy scanlines from image data into staging buffer let rect_width = loc.width() as usize; let rect_height = loc.height() as usize; let rect_x = loc.x() as usize; let rect_y = loc.y() as usize; let pixel_size = 4usize; // RGBA8 let atlas_width = extent.0 as usize; let image_data = match image { egui::ImageData::Color(color_image) => color_image.as_raw(), }; for (atlas_y, image_y) in (rect_y..rect_y + rect_height).enumerate() { let atlas_start = (image_y * atlas_width + rect_x) * pixel_size; let atlas_end = atlas_start + rect_width * pixel_size; let image_start = (atlas_y * rect_width) * pixel_size; let image_end = image_start + rect_width * pixel_size; staging_map[atlas_start..atlas_end] .copy_from_slice(&image_data[image_start..image_end]); } let graph_id = rg.import_image( textures.get_texture(id)?, Access { layout: vk::ImageLayout::GENERAL, ..Access::undefined() }, ); Some(ImageUpdate { id, graph_id, dest_offset: pos.map(|p| (p[0] as u32, p[1] as u32)).unwrap_or_default(), src_offset: (rect_x as u32, rect_y as u32), size: (rect_width as u32, rect_height as u32), }) }) .collect::>(); let writes = updates .iter() .map(|update| { ( update.graph_id, Access { layout: vk::ImageLayout::GENERAL, ..Access::undefined() }, ) }) .collect::>(); let staging_buffer = rg.import_buffer(Arc::new(staging_buffer), Access::undefined()); let staging_image = rg.import_image(Arc::new(staging_image), Access::undefined()); let record = Box::new({ // let textures = textures.clone(); move |ctx: &RenderContext| -> crate::Result<()> { let staging_image = ctx.get_image(staging_image).unwrap().clone(); let staging_buffer = ctx.get_buffer(staging_buffer).unwrap(); let image: Barrier = image_barrier( staging_image.raw(), staging_image.format(), Access { stage: vk::PipelineStageFlags2::NONE, mask: vk::AccessFlags2::empty(), layout: vk::ImageLayout::UNDEFINED, }, Access { stage: vk::PipelineStageFlags2::TRANSFER, mask: vk::AccessFlags2::TRANSFER_WRITE, layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL, }, None, ) .into(); unsafe { ctx.device .dev() .cmd_pipeline_barrier2(ctx.cmd.buffer(), &((&image).into())); } ctx.cmd.copy_buffer_to_image( staging_buffer.raw(), staging_image.raw(), vk::ImageLayout::TRANSFER_DST_OPTIMAL, &[vk::BufferImageCopy { buffer_offset: 0, buffer_row_length: staging_image.width(), buffer_image_height: staging_image.height(), image_subresource: vk::ImageSubresourceLayers::default() .aspect_mask(vk::ImageAspectFlags::COLOR) .base_array_layer(0) .mip_level(0) .layer_count(1), image_offset: vk::Offset3D { x: 0, y: 0, z: 0 }, image_extent: staging_image.size(), }], ); let from_barrier: Barrier = image_barrier( staging_image.raw(), staging_image.format(), Access { stage: vk::PipelineStageFlags2::TRANSFER, mask: vk::AccessFlags2::TRANSFER_WRITE, layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL, }, Access { stage: vk::PipelineStageFlags2::TRANSFER, mask: vk::AccessFlags2::TRANSFER_READ, layout: vk::ImageLayout::TRANSFER_SRC_OPTIMAL, }, None, ) .into(); unsafe { ctx.device .dev() .cmd_pipeline_barrier2(ctx.cmd.buffer(), &((&from_barrier).into())); } for ImageUpdate { graph_id, dest_offset, src_offset, size, .. } in &updates { let texture = ctx.get_image(*graph_id).unwrap(); let to_barrier: Barrier = image_barrier( texture.raw(), texture.format(), Access { stage: vk::PipelineStageFlags2::NONE, mask: vk::AccessFlags2::empty(), // TODO: this is somewhat sub-optimal, but I think // perfectly legal to not-care about the layout of a // texture? In this case, the entire texture is // overwritten, so the layout doesn't matter, and else // the layout will be `GENERAL` because the texture was // previously written to. layout: if staging_image.size() == texture.size() { vk::ImageLayout::UNDEFINED } else { vk::ImageLayout::GENERAL }, }, Access { stage: vk::PipelineStageFlags2::TRANSFER, mask: vk::AccessFlags2::TRANSFER_WRITE, layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL, }, None, ) .into(); unsafe { ctx.device .dev() .cmd_pipeline_barrier2(ctx.cmd.buffer(), &((&to_barrier).into())); } ctx.cmd.copy_images( staging_image.raw(), vk::ImageLayout::TRANSFER_SRC_OPTIMAL, texture.raw(), vk::ImageLayout::TRANSFER_DST_OPTIMAL, &[vk::ImageCopy { src_subresource: vk::ImageSubresourceLayers::default() .aspect_mask(vk::ImageAspectFlags::COLOR) .base_array_layer(0) .mip_level(0) .layer_count(1), src_offset: vk::Offset3D { x: src_offset.0 as i32, y: src_offset.1 as i32, z: 0, }, dst_subresource: vk::ImageSubresourceLayers::default() .aspect_mask(vk::ImageAspectFlags::COLOR) .base_array_layer(0) .mip_level(0) .layer_count(1), dst_offset: vk::Offset3D { x: dest_offset.0 as i32, y: dest_offset.1 as i32, z: 0, }, extent: vk::Extent3D { width: size.0, height: size.1, depth: 1, }, }], ); let image: Barrier = image_barrier( texture.raw(), texture.format(), Access { stage: vk::PipelineStageFlags2::TRANSFER, mask: vk::AccessFlags2::TRANSFER_WRITE, layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL, }, Access { stage: vk::PipelineStageFlags2::ALL_COMMANDS, mask: vk::AccessFlags2::empty(), layout: vk::ImageLayout::GENERAL, }, None, ) .into(); unsafe { ctx.device .dev() .cmd_pipeline_barrier2(ctx.cmd.buffer(), &((&image).into())); } } Ok(()) } }); rg.add_pass(PassDesc { reads: [ ( staging_buffer, Access { stage: vk::PipelineStageFlags2::TRANSFER, mask: vk::AccessFlags2::TRANSFER_READ, ..Access::undefined() }, ), (staging_image, Access::undefined()), ] .to_vec(), writes, record: Some(record), }); Ok(()) } // fn egui_pass() #[allow(clippy::too_many_arguments)] pub fn egui_pass( dev: &device::Device, rg: &mut RenderGraph, texture_handler: &mut crate::texture::TextureManager, samplers: &mut crate::SamplerCache, egui_state: &mut EguiState, egui: &egui::Context, output: egui::FullOutput, target: GraphResourceId, ) -> crate::Result> { let draw_data = egui.tessellate(output.shapes, output.pixels_per_point); #[repr(C)] #[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] struct Vertex { pos: glam::Vec2, uv: glam::Vec2, color: egui::epaint::Color32, } #[repr(transparent)] #[derive(Debug, Clone, Copy)] struct DrawCall(vk::DrawIndexedIndirectCommand); unsafe impl bytemuck::Zeroable for DrawCall {} unsafe impl bytemuck::Pod for DrawCall {} let mut vertices = Vec::new(); let mut indices = Vec::new(); let mut draw_calls = Vec::new(); let mut textures = IndexMap::new(); let mut textures_indices = Vec::new(); for draw in draw_data { let egui::epaint::Primitive::Mesh(mesh) = draw.primitive else { continue; }; draw_calls.push(DrawCall(vk::DrawIndexedIndirectCommand { index_count: mesh.indices.len() as u32, instance_count: 1, first_index: indices.len() as u32, vertex_offset: vertices.len() as i32, first_instance: 0, })); vertices.extend(mesh.vertices.iter().map(|v| Vertex { pos: glam::vec2(v.pos.x, v.pos.y), uv: glam::vec2(v.uv.x, v.uv.y), color: v.color, })); indices.extend(mesh.indices); let texture = egui_state.textures.get(&mesh.texture_id).cloned().unwrap(); if !textures.contains_key(&texture.id) { textures.insert(texture.id, texture); } let idx = textures.get_index_of(&texture.id).unwrap(); textures_indices.push(idx as u32); } const TEXTURE_DESCRIPTOR_CAPACITY: usize = 10; if textures.len() > TEXTURE_DESCRIPTOR_CAPACITY { return Err(crate::Error::Todo( "egui frame references more textures than the descriptor array supports", )); } let num_draw_calls = draw_calls.len(); let vertices_size = vertices.len() * size_of::(); let indices_size = indices.len() * size_of::(); let draw_calls_size = draw_calls.len() * size_of::(); let (draw_staging, vertices, indices, draw_calls, texture_ids) = { let staging_size = vertices_size + indices_size + draw_calls_size; let mut staging = Buffer::new( dev.clone(), BufferDesc { name: Some("egui-draw-staging".into()), size: staging_size as u64, usage: vk::BufferUsageFlags::TRANSFER_SRC, mem_location: MemoryLocation::CpuToGpu, ..Default::default() }, )?; { let map = staging.map_mut().unwrap(); let (st_vertices, rest) = map.split_at_mut(vertices_size); let (st_indices, rest) = rest.split_at_mut(indices_size); let (st_drawcalls, _rest) = rest.split_at_mut(draw_calls_size); st_vertices.copy_from_slice(bytemuck::cast_slice(&vertices)); st_indices.copy_from_slice(bytemuck::cast_slice(&indices)); st_drawcalls.copy_from_slice(bytemuck::cast_slice(&draw_calls)); } let staging = rg.import_buffer(Arc::new(staging), Access::undefined()); let vertices = rg.add_resource(GraphResourceDesc::Buffer(BufferDesc { name: Some("egui-draw-vertices".into()), size: vertices_size as u64, usage: vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::VERTEX_BUFFER, mem_location: MemoryLocation::GpuOnly, ..Default::default() })); let indices = rg.add_resource(GraphResourceDesc::Buffer(BufferDesc { name: Some("egui-draw-indices".into()), size: indices_size as u64, usage: vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::INDEX_BUFFER, mem_location: MemoryLocation::GpuOnly, ..Default::default() })); let draw_calls = rg.add_resource(GraphResourceDesc::Buffer(BufferDesc { name: Some("egui-draw-draw_calls".into()), size: draw_calls_size as u64, usage: vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::INDIRECT_BUFFER, mem_location: MemoryLocation::GpuOnly, ..Default::default() })); let mut texture_ids = Buffer::new( dev.clone(), BufferDesc { name: Some("egui-draw-texture_ids".into()), size: (textures_indices.len() * size_of::()) as u64, usage: vk::BufferUsageFlags::STORAGE_BUFFER, mem_location: MemoryLocation::CpuToGpu, ..Default::default() }, )?; { let map = texture_ids .map_mut() .expect("texture id buffer should be device visible"); map.copy_from_slice(bytemuck::cast_slice(&textures_indices)); } (staging, vertices, indices, draw_calls, texture_ids) }; // This map contains only unique textures referenced by these meshes. Its insertion order // is the exact index stored in `texture_ids`, matching `texture[index]` in egui.slang. let descriptor_resources = textures .values() .map(|entry| { let texture = texture_handler.get_texture(entry.id).unwrap(); let view = Arc::new(ManuallyDrop::into_inner( texture .create_view( ImageViewDesc::color_2d() .with_mip_range(0..1) .with_layer_range(0..1), ) .unwrap(), )); let sampler = samplers.get_sampler_owned(entry.as_sampler_desc()).unwrap(); (view, sampler) }) .collect::>(); let texture_ids = Arc::new(texture_ids); // Each pass gets its own managed set, so later passes/frames cannot mutate the contents // captured by this callback. let descriptor_pool = crate::pipeline::DescriptorPool::new( dev.clone(), crate::pipeline::DescriptorPoolDesc { flags: vk::DescriptorPoolCreateFlags::UPDATE_AFTER_BIND, name: Some("egui-pass-descriptor-pool".into()), sizes: &[ vk::DescriptorPoolSize { ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER, descriptor_count: TEXTURE_DESCRIPTOR_CAPACITY as u32, }, vk::DescriptorPoolSize { ty: vk::DescriptorType::STORAGE_BUFFER, descriptor_count: 1, }, ], max_sets: 1, }, )?; let mut descriptor_set = descriptor_pool .allocate_managed(crate::pipeline::DescriptorSetAllocDesc { name: None, layout: &egui_state.descriptor_layout, }) .map_err(|_| crate::Error::Todo("egui managed descriptor allocation failed"))?; { let mut writer = descriptor_set .writer() .map_err(|_| crate::Error::Todo("egui descriptor set is stale"))?; writer .clear_binding(EguiState::TEXTURE_BINDING) .map_err(|_| crate::Error::Todo("egui descriptor binding clear failed"))?; // Managed bindings are partially bound, but Vulkan descriptor contents do not have an // unset operation. Populate every unused slot with a live descriptor to avoid stale // shader-array entries while preserving the mesh texture indices above. if let Some((first_view, first_sampler)) = descriptor_resources.first() { for array_element in descriptor_resources.len()..TEXTURE_DESCRIPTOR_CAPACITY { writer .write_image( EguiState::TEXTURE_BINDING, array_element as u32, &[crate::pipeline::ImageDescriptor { view: first_view, sampler: Some(first_sampler), layout: vk::ImageLayout::GENERAL, }], ) .map_err(|_| crate::Error::Todo("egui descriptor image padding failed"))?; } } for (array_element, (view, sampler)) in descriptor_resources.iter().enumerate() { writer .write_image( EguiState::TEXTURE_BINDING, array_element as u32, &[crate::pipeline::ImageDescriptor { view, sampler: Some(sampler), layout: vk::ImageLayout::GENERAL, }], ) .map_err(|_| crate::Error::Todo("egui descriptor image write failed"))?; } writer .write_buffer( EguiState::UNIFORM_BINDING, 0, &[crate::pipeline::BufferDescriptor { buffer: &texture_ids, offset: 0, range: texture_ids.len(), }], ) .map_err(|_| crate::Error::Todo("egui descriptor buffer write failed"))?; writer .commit() .map_err(|_| crate::Error::Todo("egui descriptor commit failed"))?; } let descriptor_set_raw = descriptor_set .raw() .map_err(|_| crate::Error::Todo("egui descriptor set is stale"))?; let descriptor_set_owner = Arc::new(descriptor_set); let to_remove_tex_ids = output .textures_delta .free .iter() .filter_map(|id| egui_state.textures.get(id).cloned()) .map(|entry| entry.id) .collect::>(); let reads = textures .keys() .filter_map(|id| { texture_handler .get_texture(*id) .map(|img| (rg.import_image(img, Access::general()), Access::general())) }) .chain([(target, Access::color_attachment_read_write())]) .collect::>(); let writes = [(target, Access::color_attachment_write_only())].to_vec(); let record: Box = Box::new({ let pipeline = egui_state.pipeline.clone(); let descriptor_set_owner = descriptor_set_owner.clone(); let screen_rect = egui.screen_rect(); move |ctx: &RenderContext| -> crate::Result<()> { // Keep this managed wrapper alive through recording, and validate its generation // immediately before binding the raw handle captured for this frame. let _descriptor_set = descriptor_set_owner .raw() .map_err(|_| crate::Error::Todo("egui descriptor set is stale"))?; let cmd = &ctx.cmd; let staging = ctx.get_buffer(draw_staging).unwrap(); let vertices = ctx.get_buffer(vertices).unwrap(); let indices = ctx.get_buffer(indices).unwrap(); let draw_calls = ctx.get_buffer(draw_calls).unwrap(); let target = ctx.get_image(target).unwrap(); cmd.copy_buffers( staging.raw(), vertices.raw(), &[vk::BufferCopy { src_offset: 0, dst_offset: 0, size: vertices_size as u64, }], ); cmd.copy_buffers( staging.raw(), indices.raw(), &[vk::BufferCopy { src_offset: vertices_size as u64, dst_offset: 0, size: indices_size as u64, }], ); cmd.copy_buffers( staging.raw(), draw_calls.raw(), &[vk::BufferCopy { src_offset: (vertices_size + indices_size) as u64, dst_offset: 0, size: draw_calls_size as u64, }], ); let barriers = [ buffer_barrier( vertices.raw(), 0, vertices.len(), Access::transfer_write(), Access::vertex_read(), None, ), buffer_barrier( indices.raw(), 0, indices.len(), Access::transfer_write(), Access::index_read(), None, ), buffer_barrier( draw_calls.raw(), 0, draw_calls.len(), Access::transfer_write(), Access::indirect_read(), None, ), ]; unsafe { ctx.device.dev().cmd_pipeline_barrier2( cmd.buffer(), &vk::DependencyInfo::default().buffer_memory_barriers(&barriers), ); } let color_attachment = &vk::RenderingAttachmentInfo::default() .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) .image_view(target.create_view(ImageViewDesc::color_2d())?.raw()) .load_op(vk::AttachmentLoadOp::LOAD) .store_op(vk::AttachmentStoreOp::STORE); cmd.begin_rendering( vk::RenderingInfo::default() .color_attachments(core::slice::from_ref(color_attachment)) .layer_count(1) .render_area(vk::Rect2D::default().extent(target.extent_2d())), ); cmd.set_scissors(&[vk::Rect2D::default() .offset(vk::Offset2D::default()) .extent(target.extent_2d())]); cmd.set_viewport(&[vk::Viewport::default() .x(0.0) .y(0.0) .min_depth(0.0) .max_depth(1.0) .width(target.width() as f32) .height(target.height() as f32)]); cmd.bind_pipeline(&pipeline); cmd.bind_indices(indices.raw(), 0, vk::IndexType::UINT32); cmd.bind_vertex_buffers(&[vertices.raw()], &[0]); cmd.push_constants( pipeline.layout(), vk::ShaderStageFlags::VERTEX, 0, bytemuck::cast_slice( &[screen_rect.width(), screen_rect.height()].map(|f| f.to_bits()), ), ); cmd.bind_descriptor_sets( pipeline.layout(), vk::PipelineBindPoint::GRAPHICS, &[descriptor_set_raw], ); cmd.draw_indexed_indirect( draw_calls.raw(), 0, num_draw_calls as u32, size_of::() as u32, ); cmd.end_rendering(); Ok(()) } }); rg.add_pass(PassDesc { reads, writes, record: Some(record), }); Ok(to_remove_tex_ids) }