use { crate::gfx_apis::vulkan::{ VulkanError, image::{QueueFamily, QueueState, VulkanImage, VulkanImageMemory}, renderer::VulkanRenderer, shaders::BlurCompositePushConstants, }, ash::vk::{ AccessFlags2, AttachmentLoadOp, AttachmentStoreOp, BlitImageInfo2, CommandBuffer, DependencyInfoKHR, DescriptorImageInfo, DescriptorType, Extent2D, Extent3D, Filter, ImageAspectFlags, ImageBlit2, ImageCreateInfo, ImageLayout, ImageMemoryBarrier2, ImageSubresourceLayers, ImageSubresourceRange, ImageTiling, ImageType, ImageUsageFlags, ImageViewCreateInfo, ImageViewType, Offset2D, Offset3D, PipelineBindPoint, PipelineStageFlags2, Rect2D, RenderingAttachmentInfo, RenderingInfo, SampleCountFlags, ShaderStageFlags, SharingMode, Viewport, WriteDescriptorSet, }, gpu_alloc::UsageFlags, run_on_drop::on_drop, std::{cell::Cell, collections::hash_map::Entry, rc::Rc, slice}, }; const BLUR_SCRATCH_USAGE: ImageUsageFlags = ImageUsageFlags::from_raw( ImageUsageFlags::TRANSFER_SRC.as_raw() | ImageUsageFlags::TRANSFER_DST.as_raw() | ImageUsageFlags::SAMPLED.as_raw(), ); pub(super) struct BlurMaskRecord<'a> { pub(super) mask_view: ash::vk::ImageView, pub(super) mask_source_points: [[f32; 2]; 4], pub(super) target_points: [[f32; 2]; 4], pub(super) threshold: f32, pub(super) _phantom: std::marker::PhantomData<&'a ()>, } impl VulkanRenderer { fn acquire_blur_scratch( self: &Rc, width: u32, height: u32, format: ash::vk::Format, ) -> Result, VulkanError> { let key = (width, height, format); let cached = &mut *self.blur_scratch.borrow_mut(); let entry = cached.entry(key); if let Entry::Occupied(e) = &entry && let Some(img) = e.get().upgrade() { return Ok(img); } let create_info = ImageCreateInfo::default() .image_type(ImageType::TYPE_2D) .format(format) .mip_levels(1) .array_layers(1) .tiling(ImageTiling::OPTIMAL) .samples(SampleCountFlags::TYPE_1) .sharing_mode(SharingMode::EXCLUSIVE) .initial_layout(ImageLayout::UNDEFINED) .extent(Extent3D { width, height, depth: 1, }) .usage(BLUR_SCRATCH_USAGE); let image = unsafe { self.device.device.create_image(&create_info, None) }; let image = image.map_err(VulkanError::CreateImage)?; let destroy_image = on_drop(|| unsafe { self.device.device.destroy_image(image, None) }); let memory_requirements = unsafe { self.device.device.get_image_memory_requirements(image) }; let allocation = self.allocator .alloc(&memory_requirements, UsageFlags::FAST_DEVICE_ACCESS, false)?; let res = unsafe { self.device .device .bind_image_memory(image, allocation.memory, allocation.offset) }; res.map_err(VulkanError::BindImageMemory)?; // No view needed (we only blit), but VulkanImage requires one. let image_view_create_info = ImageViewCreateInfo::default() .image(image) .format(format) .view_type(ImageViewType::TYPE_2D) .subresource_range(ImageSubresourceRange { aspect_mask: ImageAspectFlags::COLOR, base_mip_level: 0, level_count: 1, base_array_layer: 0, layer_count: 1, }); let view = unsafe { self.device .device .create_image_view(&image_view_create_info, None) }; let view = view.map_err(VulkanError::CreateImageView)?; destroy_image.forget(); // Reuse the BLEND_FORMAT placeholder; the format field is informational // here, blit ops use the actual VkFormat above. let img = Rc::new(VulkanImage { renderer: self.clone(), format: crate::gfx_apis::vulkan::format::BLEND_FORMAT, width, height, stride: 0, texture_view: view, render_view: None, image, is_undefined: Cell::new(true), contents_are_undefined: Cell::new(true), queue_state: Cell::new(QueueState::Acquired { family: QueueFamily::Gfx, }), ty: VulkanImageMemory::Blend(allocation), bridge: None, sampled_image_descriptor: None, execution_version: Default::default(), }); match entry { Entry::Occupied(mut e) => { e.insert(Rc::downgrade(&img)); } Entry::Vacant(e) => { e.insert(Rc::downgrade(&img)); } } Ok(img) } /// Records a backdrop blur of the given pixel rect on the target image. /// Caller is responsible for ending the current dynamic render pass before /// invoking, and for restarting it afterward (with LOAD). pub(super) fn record_blur( self: &Rc, buf: CommandBuffer, target: &VulkanImage, rect: [i32; 4], passes: u8, scratch_out: &mut Vec>, mask: Option<&BlurMaskRecord<'_>>, ) -> Result<(), VulkanError> { let [x1, y1, x2, y2] = rect; let x1 = x1.max(0).min(target.width as i32); let y1 = y1.max(0).min(target.height as i32); let x2 = x2.max(0).min(target.width as i32); let y2 = y2.max(0).min(target.height as i32); let w = (x2 - x1) as u32; let h = (y2 - y1) as u32; if w < 4 || h < 4 { return Ok(()); } let passes = passes.clamp(1, 6) as u32; let format = target.format.vk_format; let mut levels: Vec> = Vec::with_capacity(passes as usize + 1); levels.push(self.acquire_blur_scratch(w, h, format)?); let mut cw = w; let mut ch = h; for _ in 0..passes { cw = (cw / 2).max(1); ch = (ch / 2).max(1); levels.push(self.acquire_blur_scratch(cw, ch, format)?); } let dev = &self.device.device; let subres = ImageSubresourceLayers::default() .aspect_mask(ImageAspectFlags::COLOR) .layer_count(1) .base_array_layer(0) .mip_level(0); let subres_range = ImageSubresourceRange { aspect_mask: ImageAspectFlags::COLOR, base_mip_level: 0, level_count: 1, base_array_layer: 0, layer_count: 1, }; let barrier = |image, old: ImageLayout, new: ImageLayout, src_stage: PipelineStageFlags2, src_access: AccessFlags2, dst_stage: PipelineStageFlags2, dst_access: AccessFlags2| { ImageMemoryBarrier2::default() .image(image) .old_layout(old) .new_layout(new) .subresource_range(subres_range) .src_stage_mask(src_stage) .src_access_mask(src_access) .dst_stage_mask(dst_stage) .dst_access_mask(dst_access) .src_queue_family_index(ash::vk::QUEUE_FAMILY_IGNORED) .dst_queue_family_index(ash::vk::QUEUE_FAMILY_IGNORED) }; let do_barriers = |barriers: &[ImageMemoryBarrier2]| unsafe { let dep = DependencyInfoKHR::default().image_memory_barriers(barriers); dev.cmd_pipeline_barrier2(buf, &dep); }; // Step 1: target COLOR_ATTACHMENT -> TRANSFER_SRC. // Step 1: levels[0] UNDEFINED -> TRANSFER_DST. do_barriers(&[ barrier( target.image, ImageLayout::COLOR_ATTACHMENT_OPTIMAL, ImageLayout::TRANSFER_SRC_OPTIMAL, PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT, AccessFlags2::COLOR_ATTACHMENT_WRITE, PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_READ, ), barrier( levels[0].image, ImageLayout::UNDEFINED, ImageLayout::TRANSFER_DST_OPTIMAL, PipelineStageFlags2::TOP_OF_PIPE, AccessFlags2::empty(), PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_WRITE, ), ]); // Step 2: blit target rect -> levels[0] full. let blit = ImageBlit2::default() .src_subresource(subres) .dst_subresource(subres) .src_offsets([ Offset3D { x: x1, y: y1, z: 0 }, Offset3D { x: x2, y: y2, z: 1 }, ]) .dst_offsets([ Offset3D { x: 0, y: 0, z: 0 }, Offset3D { x: w as i32, y: h as i32, z: 1, }, ]); let blit_info = BlitImageInfo2::default() .src_image(target.image) .src_image_layout(ImageLayout::TRANSFER_SRC_OPTIMAL) .dst_image(levels[0].image) .dst_image_layout(ImageLayout::TRANSFER_DST_OPTIMAL) .filter(Filter::LINEAR) .regions(slice::from_ref(&blit)); unsafe { dev.cmd_blit_image2(buf, &blit_info); } // Down passes: levels[i-1] -> levels[i] with linear filter. for i in 1..=passes as usize { let (src, dst) = (&levels[i - 1], &levels[i]); do_barriers(&[ barrier( src.image, ImageLayout::TRANSFER_DST_OPTIMAL, ImageLayout::TRANSFER_SRC_OPTIMAL, PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_WRITE, PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_READ, ), barrier( dst.image, ImageLayout::UNDEFINED, ImageLayout::TRANSFER_DST_OPTIMAL, PipelineStageFlags2::TOP_OF_PIPE, AccessFlags2::empty(), PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_WRITE, ), ]); let blit = ImageBlit2::default() .src_subresource(subres) .dst_subresource(subres) .src_offsets([ Offset3D { x: 0, y: 0, z: 0 }, Offset3D { x: src.width as i32, y: src.height as i32, z: 1, }, ]) .dst_offsets([ Offset3D { x: 0, y: 0, z: 0 }, Offset3D { x: dst.width as i32, y: dst.height as i32, z: 1, }, ]); let blit_info = BlitImageInfo2::default() .src_image(src.image) .src_image_layout(ImageLayout::TRANSFER_SRC_OPTIMAL) .dst_image(dst.image) .dst_image_layout(ImageLayout::TRANSFER_DST_OPTIMAL) .filter(Filter::LINEAR) .regions(slice::from_ref(&blit)); unsafe { dev.cmd_blit_image2(buf, &blit_info); } } // Up passes: levels[i+1] -> levels[i] with linear filter. for i in (0..passes as usize).rev() { let (src, dst) = (&levels[i + 1], &levels[i]); do_barriers(&[ barrier( src.image, ImageLayout::TRANSFER_DST_OPTIMAL, ImageLayout::TRANSFER_SRC_OPTIMAL, PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_WRITE, PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_READ, ), barrier( dst.image, ImageLayout::TRANSFER_SRC_OPTIMAL, ImageLayout::TRANSFER_DST_OPTIMAL, PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_READ, PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_WRITE, ), ]); let blit = ImageBlit2::default() .src_subresource(subres) .dst_subresource(subres) .src_offsets([ Offset3D { x: 0, y: 0, z: 0 }, Offset3D { x: src.width as i32, y: src.height as i32, z: 1, }, ]) .dst_offsets([ Offset3D { x: 0, y: 0, z: 0 }, Offset3D { x: dst.width as i32, y: dst.height as i32, z: 1, }, ]); let blit_info = BlitImageInfo2::default() .src_image(src.image) .src_image_layout(ImageLayout::TRANSFER_SRC_OPTIMAL) .dst_image(dst.image) .dst_image_layout(ImageLayout::TRANSFER_DST_OPTIMAL) .filter(Filter::LINEAR) .regions(slice::from_ref(&blit)); unsafe { dev.cmd_blit_image2(buf, &blit_info); } } if let Some(mask) = mask { // Masked composite path: // levels[0] (TRANSFER_DST) -> SHADER_READ_ONLY_OPTIMAL // target (TRANSFER_SRC) -> COLOR_ATTACHMENT_OPTIMAL // draw composite shader sampling levels[0] + mask, blending onto fb do_barriers(&[ barrier( levels[0].image, ImageLayout::TRANSFER_DST_OPTIMAL, ImageLayout::SHADER_READ_ONLY_OPTIMAL, PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_WRITE, PipelineStageFlags2::FRAGMENT_SHADER, AccessFlags2::SHADER_SAMPLED_READ, ), barrier( target.image, ImageLayout::TRANSFER_SRC_OPTIMAL, ImageLayout::COLOR_ATTACHMENT_OPTIMAL, PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_READ, PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT, AccessFlags2::COLOR_ATTACHMENT_WRITE | AccessFlags2::COLOR_ATTACHMENT_READ, ), ]); let pipeline = self.get_or_create_blur_composite_pipeline(target.format.vk_format)?; let target_render_view = target .render_view .unwrap_or(target.texture_view); let color_attachment = RenderingAttachmentInfo::default() .image_view(target_render_view) .image_layout(ImageLayout::COLOR_ATTACHMENT_OPTIMAL) .load_op(AttachmentLoadOp::LOAD) .store_op(AttachmentStoreOp::STORE); let render_area = Rect2D { offset: Offset2D { x: 0, y: 0 }, extent: Extent2D { width: target.width, height: target.height, }, }; let rendering_info = RenderingInfo::default() .render_area(render_area) .layer_count(1) .color_attachments(slice::from_ref(&color_attachment)); let viewport = Viewport { x: 0.0, y: 0.0, width: target.width as f32, height: target.height as f32, min_depth: 0.0, max_depth: 1.0, }; let scissor = Rect2D { offset: Offset2D { x: x1, y: y1 }, extent: Extent2D { width: w, height: h, }, }; // Identity uv across blurred level 0 (full image is the blurred rect). let blurred_tc: [[f32; 2]; 4] = [ [1.0, 0.0], [0.0, 0.0], [1.0, 1.0], [0.0, 1.0], ]; let push = BlurCompositePushConstants { pos: mask.target_points, blurred_tex_pos: blurred_tc, mask_tex_pos: mask.mask_source_points, threshold: mask.threshold, }; let blurred_image_info = DescriptorImageInfo::default() .image_view(levels[0].texture_view) .image_layout(ImageLayout::SHADER_READ_ONLY_OPTIMAL); let mask_image_info = DescriptorImageInfo::default() .image_view(mask.mask_view) .image_layout(ImageLayout::SHADER_READ_ONLY_OPTIMAL); let writes = [ WriteDescriptorSet::default() .dst_binding(0) .descriptor_type(DescriptorType::COMBINED_IMAGE_SAMPLER) .image_info(slice::from_ref(&blurred_image_info)), WriteDescriptorSet::default() .dst_binding(1) .descriptor_type(DescriptorType::COMBINED_IMAGE_SAMPLER) .image_info(slice::from_ref(&mask_image_info)), ]; unsafe { dev.cmd_begin_rendering(buf, &rendering_info); dev.cmd_bind_pipeline(buf, PipelineBindPoint::GRAPHICS, pipeline.pipeline); dev.cmd_set_viewport(buf, 0, slice::from_ref(&viewport)); dev.cmd_set_scissor(buf, 0, slice::from_ref(&scissor)); self.device.push_descriptor.cmd_push_descriptor_set( buf, PipelineBindPoint::GRAPHICS, pipeline.pipeline_layout, 0, &writes, ); dev.cmd_push_constants( buf, pipeline.pipeline_layout, ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT, 0, uapi::as_bytes(&push), ); dev.cmd_draw(buf, 4, 1, 0, 0); dev.cmd_end_rendering(buf); } } else { // Final blit: levels[0] -> target rect. do_barriers(&[ barrier( levels[0].image, ImageLayout::TRANSFER_DST_OPTIMAL, ImageLayout::TRANSFER_SRC_OPTIMAL, PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_WRITE, PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_READ, ), barrier( target.image, ImageLayout::TRANSFER_SRC_OPTIMAL, ImageLayout::TRANSFER_DST_OPTIMAL, PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_READ, PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_WRITE, ), ]); let blit = ImageBlit2::default() .src_subresource(subres) .dst_subresource(subres) .src_offsets([ Offset3D { x: 0, y: 0, z: 0 }, Offset3D { x: w as i32, y: h as i32, z: 1, }, ]) .dst_offsets([ Offset3D { x: x1, y: y1, z: 0 }, Offset3D { x: x2, y: y2, z: 1 }, ]); let blit_info = BlitImageInfo2::default() .src_image(levels[0].image) .src_image_layout(ImageLayout::TRANSFER_SRC_OPTIMAL) .dst_image(target.image) .dst_image_layout(ImageLayout::TRANSFER_DST_OPTIMAL) .filter(Filter::NEAREST) .regions(slice::from_ref(&blit)); unsafe { dev.cmd_blit_image2(buf, &blit_info); } // Restore target to COLOR_ATTACHMENT_OPTIMAL. do_barriers(&[barrier( target.image, ImageLayout::TRANSFER_DST_OPTIMAL, ImageLayout::COLOR_ATTACHMENT_OPTIMAL, PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_WRITE, PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT, AccessFlags2::COLOR_ATTACHMENT_WRITE | AccessFlags2::COLOR_ATTACHMENT_READ, )]); } // Hold the scratch images until the frame is submitted. scratch_out.extend(levels); Ok(()) } }