From e2de6883245b06ebd43c2be11127350289a1f271 Mon Sep 17 00:00:00 2001 From: entailz Date: Wed, 20 May 2026 18:44:47 -0700 Subject: [PATCH 1/4] Replace the blit cascade in record_blur with shader-based dual-Kawase, matching the OpenGL backend's algorithm. The blit cascade was a much weaker filter that degenerated badly on thin surfaces. The new path adds blur.vert / blur_down.frag (5-tap) / blur_up.frag (8-tap), a single-binding blur descriptor set layout, and per-format down/up pipelines. BLUR_SCRATCH_USAGE gains COLOR_ATTACHMENT so the scratch images can be both sampled and rendered into. Cache hit fast path and masked composite are unchanged. --- build/vulkan/hash.rs | 3 + src/gfx_apis/vulkan/blur.rs | 509 ++++++++++++------ src/gfx_apis/vulkan/descriptor.rs | 25 + src/gfx_apis/vulkan/renderer.rs | 256 ++++++++- src/gfx_apis/vulkan/shaders.rs | 16 + src/gfx_apis/vulkan/shaders/blur.vert | 26 + src/gfx_apis/vulkan/shaders/blur_down.frag | 21 + src/gfx_apis/vulkan/shaders/blur_up.frag | 24 + src/gfx_apis/vulkan/shaders_bin/blur.vert.spv | Bin 0 -> 1412 bytes .../vulkan/shaders_bin/blur_down.frag.spv | Bin 0 -> 2052 bytes .../vulkan/shaders_bin/blur_up.frag.spv | Bin 0 -> 3036 bytes src/gfx_apis/vulkan/shaders_hash.txt | 3 + 12 files changed, 707 insertions(+), 176 deletions(-) create mode 100644 src/gfx_apis/vulkan/shaders/blur.vert create mode 100644 src/gfx_apis/vulkan/shaders/blur_down.frag create mode 100644 src/gfx_apis/vulkan/shaders/blur_up.frag create mode 100644 src/gfx_apis/vulkan/shaders_bin/blur.vert.spv create mode 100644 src/gfx_apis/vulkan/shaders_bin/blur_down.frag.spv create mode 100644 src/gfx_apis/vulkan/shaders_bin/blur_up.frag.spv diff --git a/build/vulkan/hash.rs b/build/vulkan/hash.rs index 1a1d5573..edd5f2e2 100644 --- a/build/vulkan/hash.rs +++ b/build/vulkan/hash.rs @@ -25,6 +25,9 @@ pub const TREES: &[Tree] = &[Tree { "rounded_tex.vert", "blur_composite.vert", "blur_composite.frag", + "blur.vert", + "blur_down.frag", + "blur_up.frag", "legacy/fill.frag", "legacy/fill.vert", "legacy/tex.vert", diff --git a/src/gfx_apis/vulkan/blur.rs b/src/gfx_apis/vulkan/blur.rs index b63cc924..af781cea 100644 --- a/src/gfx_apis/vulkan/blur.rs +++ b/src/gfx_apis/vulkan/blur.rs @@ -2,8 +2,9 @@ use { crate::gfx_apis::vulkan::{ VulkanError, image::{QueueFamily, QueueState, VulkanImage, VulkanImageMemory}, + pipeline::VulkanPipeline, renderer::VulkanRenderer, - shaders::BlurCompositePushConstants, + shaders::{BlurCompositePushConstants, BlurPushConstants}, }, ash::vk::{ AccessFlags2, AttachmentLoadOp, AttachmentStoreOp, BlitImageInfo2, CommandBuffer, @@ -16,13 +17,14 @@ use { }, gpu_alloc::UsageFlags, run_on_drop::on_drop, - std::{cell::Cell, collections::hash_map::Entry, rc::Rc, slice}, + std::{cell::Cell, 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(), + | ImageUsageFlags::SAMPLED.as_raw() + | ImageUsageFlags::COLOR_ATTACHMENT.as_raw(), ); pub(super) struct BlurMaskRecord<'a> { @@ -42,12 +44,17 @@ impl VulkanRenderer { ) -> 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); + + if let Some(weak) = cached.get(&key) { + if let Some(img) = weak.upgrade() { + if Rc::strong_count(&img) == 1 { + img.is_undefined.set(false); + img.contents_are_undefined.set(false); + return Ok(img); + } + } } + let create_info = ImageCreateInfo::default() .image_type(ImageType::TYPE_2D) .format(format) @@ -77,7 +84,6 @@ impl VulkanRenderer { .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) @@ -96,8 +102,6 @@ impl VulkanRenderer { }; 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, @@ -117,28 +121,30 @@ impl VulkanRenderer { 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)); - } - } + cached.insert(key, 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). + /// Records a dual-Kawase 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). + /// + /// If `cached_blur` is Some, the cascade is skipped and that image is used + /// directly as the blurred input to the composite. The mask must also be Some + /// in that case, since cache+no-mask is just a no-op (blit-back of cached). + /// On a cache miss, the level-0 scratch image (holding the blurred result) + /// is returned via `out_blur_image` for the caller to store in the cache. pub(super) fn record_blur( self: &Rc, buf: CommandBuffer, target: &VulkanImage, rect: [i32; 4], passes: u8, + offset: f32, scratch_out: &mut Vec>, mask: Option<&BlurMaskRecord<'_>>, + cached_blur: Option<&Rc>, + out_blur_image: &mut Option>, ) -> Result<(), VulkanError> { let [x1, y1, x2, y2] = rect; let x1 = x1.max(0).min(target.width as i32); @@ -150,7 +156,26 @@ impl VulkanRenderer { if w < 4 || h < 4 { return Ok(()); } - let passes = passes.clamp(1, 6) as u32; + let passes = passes.clamp(1, 8) as u32; + let offset = offset.max(0.0); + + let dev = &self.device.device; + + // Cache hit fast path: skip cascade, just composite from cached image. + // The format check matters because BlurBarrier may run in either the + // BlendBuffer pass (linear format) or the FrameBuffer pass (gamma) + // depending on whether BB was elided. A cached image from one pass + // has the wrong format for the other. + if let (Some(cached), Some(mask)) = (cached_blur, mask) + && cached.width == w + && cached.height == h + && cached.format.vk_format == target.format.vk_format + { + self.record_blur_composite_only(buf, target, cached, mask, [x1, y1, x2, y2])?; + // Hold cached image alive for this frame's GPU execution. + scratch_out.push(cached.clone()); + return Ok(()); + } let format = target.format.vk_format; let mut levels: Vec> = Vec::with_capacity(passes as usize + 1); @@ -163,7 +188,8 @@ impl VulkanRenderer { levels.push(self.acquire_blur_scratch(cw, ch, format)?); } - let dev = &self.device.device; + // After cascade, levels[0] holds the blurred result. Stash it for caching. + *out_blur_image = Some(levels[0].clone()); let subres = ImageSubresourceLayers::default() .aspect_mask(ImageAspectFlags::COLOR) .layer_count(1) @@ -203,7 +229,7 @@ impl VulkanRenderer { }; // Step 1: target COLOR_ATTACHMENT -> TRANSFER_SRC. - // Step 1: levels[0] UNDEFINED -> TRANSFER_DST. + // levels[0] -> TRANSFER_DST (discard prior contents). do_barriers(&[ barrier( target.image, @@ -224,6 +250,7 @@ impl VulkanRenderer { AccessFlags2::TRANSFER_WRITE, ), ]); + levels[0].is_undefined.set(false); // Step 2: blit target rect -> levels[0] full. let blit = ImageBlit2::default() @@ -252,139 +279,153 @@ impl VulkanRenderer { 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)); + // Step 3: levels[0] TRANSFER_DST -> SHADER_READ_ONLY for sampling in + // the down pass. + 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, + )]); + + let blur_down_pipeline = self.get_or_create_blur_down_pipeline(format)?; + let blur_up_pipeline = self.get_or_create_blur_up_pipeline(format)?; + + // Helper to run one blur pass: sample `src`, draw into `dst`. Caller + // must have transitioned dst to COLOR_ATTACHMENT and src to + // SHADER_READ_ONLY before this. Layouts after: dst stays in + // COLOR_ATTACHMENT (caller transitions next). + let run_pass = |pipeline: &VulkanPipeline, + src: &VulkanImage, + dst: &VulkanImage| + -> Result<(), VulkanError> { + let color_attachment = RenderingAttachmentInfo::default() + .image_view(dst.texture_view) + .image_layout(ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .load_op(AttachmentLoadOp::DONT_CARE) + .store_op(AttachmentStoreOp::STORE); + let render_area = Rect2D { + offset: Offset2D { x: 0, y: 0 }, + extent: Extent2D { + width: dst.width, + height: dst.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: dst.width as f32, + height: dst.height as f32, + min_depth: 0.0, + max_depth: 1.0, + }; + let scissor = render_area; + let push = BlurPushConstants { + halfpixel: [0.5 / src.width as f32, 0.5 / src.height as f32], + offset, + }; + let src_image_info = DescriptorImageInfo::default() + .image_view(src.texture_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(&src_image_info))]; unsafe { - dev.cmd_blit_image2(buf, &blit_info); + 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::FRAGMENT, + 0, + uapi::as_bytes(&push), + ); + dev.cmd_draw(buf, 4, 1, 0, 0); + dev.cmd_end_rendering(buf); } + Ok(()) + }; + + // Down passes: levels[i-1] (SHADER_READ_ONLY) -> levels[i] (COLOR_ATT). + // Each iteration transitions the destination to COLOR_ATTACHMENT, + // draws, then to SHADER_READ_ONLY for the next iteration's read. + for i in 1..=passes as usize { + do_barriers(&[barrier( + levels[i].image, + ImageLayout::UNDEFINED, + ImageLayout::COLOR_ATTACHMENT_OPTIMAL, + PipelineStageFlags2::TOP_OF_PIPE, + AccessFlags2::empty(), + PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT, + AccessFlags2::COLOR_ATTACHMENT_WRITE, + )]); + levels[i].is_undefined.set(false); + run_pass(&blur_down_pipeline, &levels[i - 1], &levels[i])?; + do_barriers(&[barrier( + levels[i].image, + ImageLayout::COLOR_ATTACHMENT_OPTIMAL, + ImageLayout::SHADER_READ_ONLY_OPTIMAL, + PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT, + AccessFlags2::COLOR_ATTACHMENT_WRITE, + PipelineStageFlags2::FRAGMENT_SHADER, + AccessFlags2::SHADER_SAMPLED_READ, + )]); } - // Up passes: levels[i+1] -> levels[i] with linear filter. + // Up passes: levels[i+1] (SHADER_READ_ONLY) -> levels[i] (COLOR_ATT). 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); - } + do_barriers(&[barrier( + levels[i].image, + ImageLayout::SHADER_READ_ONLY_OPTIMAL, + ImageLayout::COLOR_ATTACHMENT_OPTIMAL, + PipelineStageFlags2::FRAGMENT_SHADER, + AccessFlags2::SHADER_SAMPLED_READ, + PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT, + AccessFlags2::COLOR_ATTACHMENT_WRITE, + )]); + run_pass(&blur_up_pipeline, &levels[i + 1], &levels[i])?; + do_barriers(&[barrier( + levels[i].image, + ImageLayout::COLOR_ATTACHMENT_OPTIMAL, + ImageLayout::SHADER_READ_ONLY_OPTIMAL, + PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT, + AccessFlags2::COLOR_ATTACHMENT_WRITE, + PipelineStageFlags2::FRAGMENT_SHADER, + AccessFlags2::SHADER_SAMPLED_READ, + )]); } + // After cascade: levels[0] in SHADER_READ_ONLY, target in TRANSFER_SRC. + 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, - ), - ]); + // Masked composite path: restore target to COLOR_ATTACHMENT and + // draw the composite shader sampling levels[0] + mask. + do_barriers(&[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)?; @@ -470,14 +511,15 @@ impl VulkanRenderer { dev.cmd_end_rendering(buf); } } else { - // Final blit: levels[0] -> target rect. + // Unmasked: transition levels[0] back to TRANSFER_SRC, target stays + // in TRANSFER_SRC, retarget target to TRANSFER_DST, blit-back. do_barriers(&[ barrier( levels[0].image, - ImageLayout::TRANSFER_DST_OPTIMAL, + ImageLayout::SHADER_READ_ONLY_OPTIMAL, ImageLayout::TRANSFER_SRC_OPTIMAL, - PipelineStageFlags2::TRANSFER, - AccessFlags2::TRANSFER_WRITE, + PipelineStageFlags2::FRAGMENT_SHADER, + AccessFlags2::SHADER_SAMPLED_READ, PipelineStageFlags2::TRANSFER, AccessFlags2::TRANSFER_READ, ), @@ -516,21 +558,158 @@ impl VulkanRenderer { 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, - )]); + // Restore target to COLOR_ATTACHMENT for the resumed render pass. + // Also push levels[0] back to SHADER_READ_ONLY so its tracked layout + // matches what the cache-hit fast path expects on next frame. + 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, + ), + barrier( + levels[0].image, + ImageLayout::TRANSFER_SRC_OPTIMAL, + ImageLayout::SHADER_READ_ONLY_OPTIMAL, + PipelineStageFlags2::TRANSFER, + AccessFlags2::TRANSFER_READ, + PipelineStageFlags2::FRAGMENT_SHADER, + AccessFlags2::SHADER_SAMPLED_READ, + ), + ]); } // Hold the scratch images until the frame is submitted. scratch_out.extend(levels); Ok(()) } + + /// Cache-hit fast path. Cached image is already in SHADER_READ_ONLY_OPTIMAL + /// (the layout we leave it in after the previous frame's composite). We just + /// re-bind the composite pipeline with the cached image as input and draw. + fn record_blur_composite_only( + self: &Rc, + buf: CommandBuffer, + target: &VulkanImage, + cached: &Rc, + mask: &BlurMaskRecord<'_>, + rect: [i32; 4], + ) -> Result<(), VulkanError> { + let [x1, y1, x2, y2] = rect; + let w = (x2 - x1) as u32; + let h = (y2 - y1) as u32; + let dev = &self.device.device; + // The caller (BlurBarrier handler) has already cmd_end_rendering'd the + // pass that produced the underlying scene. Without an explicit barrier + // here, the new render pass's LOAD reads can race with those prior + // COLOR_ATTACHMENT_WRITEs and pull stale memory — which manifests as + // per-frame flicker in the blurred region. + let target_load_barrier = ImageMemoryBarrier2::default() + .image(target.image) + .old_layout(ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .new_layout(ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .subresource_range(ImageSubresourceRange { + aspect_mask: ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }) + .src_stage_mask(PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .src_access_mask(AccessFlags2::COLOR_ATTACHMENT_WRITE) + .dst_stage_mask(PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .dst_access_mask( + AccessFlags2::COLOR_ATTACHMENT_READ | AccessFlags2::COLOR_ATTACHMENT_WRITE, + ) + .src_queue_family_index(ash::vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(ash::vk::QUEUE_FAMILY_IGNORED); + let dep = DependencyInfoKHR::default() + .image_memory_barriers(slice::from_ref(&target_load_barrier)); + unsafe { + dev.cmd_pipeline_barrier2(buf, &dep); + } + 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, + }, + }; + 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(cached.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); + } + Ok(()) + } } diff --git a/src/gfx_apis/vulkan/descriptor.rs b/src/gfx_apis/vulkan/descriptor.rs index fa207f7a..145ce007 100644 --- a/src/gfx_apis/vulkan/descriptor.rs +++ b/src/gfx_apis/vulkan/descriptor.rs @@ -87,6 +87,31 @@ impl VulkanDevice { })) } + pub(super) fn create_blur_descriptor_set_layout( + self: &Rc, + sampler: &Rc, + ) -> Result, VulkanError> { + let immutable_sampler = [sampler.sampler]; + let binding = DescriptorSetLayoutBinding::default() + .binding(0) + .stage_flags(ShaderStageFlags::FRAGMENT) + .descriptor_count(1) + .descriptor_type(DescriptorType::COMBINED_IMAGE_SAMPLER) + .immutable_samplers(&immutable_sampler); + let create_info = DescriptorSetLayoutCreateInfo::default() + .bindings(slice::from_ref(&binding)) + .flags(DescriptorSetLayoutCreateFlags::PUSH_DESCRIPTOR_KHR); + let layout = unsafe { self.device.create_descriptor_set_layout(&create_info, None) }; + let layout = layout.map_err(VulkanError::CreateDescriptorSetLayout)?; + Ok(Rc::new(VulkanDescriptorSetLayout { + device: self.clone(), + layout, + size: 0, + offsets: Default::default(), + _sampler: Some(sampler.clone()), + })) + } + pub(super) fn create_tex_sampler_descriptor_set_layout( self: &Rc, sampler: &Rc, diff --git a/src/gfx_apis/vulkan/renderer.rs b/src/gfx_apis/vulkan/renderer.rs index c1de1a03..bf94d6f2 100644 --- a/src/gfx_apis/vulkan/renderer.rs +++ b/src/gfx_apis/vulkan/renderer.rs @@ -26,7 +26,8 @@ use { sampler::VulkanSampler, semaphore::VulkanSemaphore, shaders::{ - BLUR_COMPOSITE_FRAG, BLUR_COMPOSITE_VERT, BlurCompositePushConstants, + BLUR_COMPOSITE_FRAG, BLUR_COMPOSITE_VERT, BLUR_DOWN_FRAG, BLUR_UP_FRAG, BLUR_VERT, + BlurCompositePushConstants, BlurPushConstants, ColorManagementData, EotfArgs, FILL_FRAG, FILL_VERT, FillPushConstants, InvEotfArgs, LEGACY_FILL_FRAG, LEGACY_FILL_VERT, LEGACY_ROUNDED_FILL_FRAG, LEGACY_ROUNDED_FILL_VERT, LEGACY_ROUNDED_TEX_FRAG, LEGACY_ROUNDED_TEX_VERT, @@ -123,6 +124,12 @@ pub struct VulkanRenderer { pub(super) blur_composite_frag_shader: Rc, pub(super) blur_composite_descriptor_set_layout: Rc, pub(super) blur_composite_pipelines: CopyHashMap>, + pub(super) blur_vert_shader: Rc, + pub(super) blur_down_frag_shader: Rc, + pub(super) blur_up_frag_shader: Rc, + pub(super) blur_descriptor_set_layout: Rc, + pub(super) blur_down_pipelines: CopyHashMap>, + pub(super) blur_up_pipelines: CopyHashMap>, pub(super) defunct: Cell, pub(super) pending_cpu_jobs: CopyHashMap, pub(super) shm_allocator: Rc, @@ -229,13 +236,20 @@ enum VulkanOp { struct VulkanBlurOp { rect: crate::gfx_api::FramebufferRect, passes: u8, + offset: f32, mask: Option, + cache: Option>>>, + cache_epoch: u64, + cache_pixel_rect: [i32; 4], } struct VulkanBlurMask { tex: Rc, source: crate::gfx_api::SampleRect, threshold: f32, + buffer_resv: Option>, + acquire_sync: Option, + release_sync: ReleaseSync, } struct VulkanTexOp { @@ -246,6 +260,7 @@ struct VulkanTexOp { acquire_sync: Option, release_sync: ReleaseSync, alpha: f32, + discard_alpha: f32, source_type: TexSourceType, copy_type: TexCopyType, alpha_mode: AlphaMode, @@ -286,6 +301,7 @@ struct VulkanRoundedTexOp { acquire_sync: Option, release_sync: ReleaseSync, alpha: f32, + discard_alpha: f32, source_type: TexSourceType, copy_type: TexCopyType, alpha_mode: AlphaMode, @@ -406,6 +422,10 @@ impl VulkanDevice { let blur_composite_frag_shader = self.create_shader(BLUR_COMPOSITE_FRAG)?; let blur_composite_descriptor_set_layout = self.create_blur_composite_descriptor_set_layout(&sampler)?; + let blur_vert_shader = self.create_shader(BLUR_VERT)?; + let blur_down_frag_shader = self.create_shader(BLUR_DOWN_FRAG)?; + let blur_up_frag_shader = self.create_shader(BLUR_UP_FRAG)?; + let blur_descriptor_set_layout = self.create_blur_descriptor_set_layout(&sampler)?; let gfx_command_buffers = self.create_command_pool(self.graphics_queue_idx)?; let transfer_command_buffers = self .distinct_transfer_queue_family_idx @@ -497,6 +517,12 @@ impl VulkanDevice { blur_composite_frag_shader, blur_composite_descriptor_set_layout, blur_composite_pipelines: Default::default(), + blur_vert_shader, + blur_down_frag_shader, + blur_up_frag_shader, + blur_descriptor_set_layout, + blur_down_pipelines: Default::default(), + blur_up_pipelines: Default::default(), defunct: Cell::new(false), pending_cpu_jobs: Default::default(), shm_allocator, @@ -886,6 +912,128 @@ impl VulkanRenderer { })) } + pub(super) fn get_or_create_blur_down_pipeline( + &self, + format: vk::Format, + ) -> Result, VulkanError> { + if let Some(pl) = self.blur_down_pipelines.get(&format) { + return Ok(pl); + } + let pl = self.create_blur_pass_pipeline(format, &self.blur_down_frag_shader)?; + self.blur_down_pipelines.set(format, pl.clone()); + Ok(pl) + } + + pub(super) fn get_or_create_blur_up_pipeline( + &self, + format: vk::Format, + ) -> Result, VulkanError> { + if let Some(pl) = self.blur_up_pipelines.get(&format) { + return Ok(pl); + } + let pl = self.create_blur_pass_pipeline(format, &self.blur_up_frag_shader)?; + self.blur_up_pipelines.set(format, pl.clone()); + Ok(pl) + } + + fn create_blur_pass_pipeline( + &self, + format: vk::Format, + frag: &Rc, + ) -> Result, VulkanError> { + use ash::vk::{ + ColorComponentFlags, CullModeFlags, DynamicState, FrontFace, + GraphicsPipelineCreateInfo, PipelineCache, PipelineColorBlendAttachmentState, + PipelineColorBlendStateCreateInfo, PipelineDynamicStateCreateInfo, + PipelineInputAssemblyStateCreateInfo, PipelineLayoutCreateInfo, + PipelineMultisampleStateCreateInfo, PipelineRasterizationStateCreateInfo, + PipelineRenderingCreateInfo, PipelineShaderStageCreateInfo, + PipelineVertexInputStateCreateInfo, PipelineViewportStateCreateInfo, PolygonMode, + PrimitiveTopology, PushConstantRange, SampleCountFlags, + }; + let dev = &self.device.device; + let push_range = PushConstantRange::default() + .stage_flags(ShaderStageFlags::FRAGMENT) + .offset(0) + .size(size_of::() as u32); + let set_layouts = [self.blur_descriptor_set_layout.layout]; + let layout_info = PipelineLayoutCreateInfo::default() + .push_constant_ranges(slice::from_ref(&push_range)) + .set_layouts(&set_layouts); + let pipeline_layout = unsafe { dev.create_pipeline_layout(&layout_info, None) }; + let pipeline_layout = pipeline_layout.map_err(VulkanError::CreatePipelineLayout)?; + let destroy_layout = + run_on_drop::on_drop(|| unsafe { dev.destroy_pipeline_layout(pipeline_layout, None) }); + let stages = [ + PipelineShaderStageCreateInfo::default() + .stage(ShaderStageFlags::VERTEX) + .module(self.blur_vert_shader.module) + .name(c"main"), + PipelineShaderStageCreateInfo::default() + .stage(ShaderStageFlags::FRAGMENT) + .module(frag.module) + .name(c"main"), + ]; + let input_assembly_state = PipelineInputAssemblyStateCreateInfo::default() + .topology(PrimitiveTopology::TRIANGLE_STRIP); + let vertex_input_state = PipelineVertexInputStateCreateInfo::default(); + let rasterization_state = PipelineRasterizationStateCreateInfo::default() + .polygon_mode(PolygonMode::FILL) + .cull_mode(CullModeFlags::NONE) + .line_width(1.0) + .front_face(FrontFace::COUNTER_CLOCKWISE); + let multisampling_state = PipelineMultisampleStateCreateInfo::default() + .sample_shading_enable(false) + .rasterization_samples(SampleCountFlags::TYPE_1); + let blending = PipelineColorBlendAttachmentState::default() + .color_write_mask(ColorComponentFlags::RGBA) + .blend_enable(false); + let color_blend_state = + PipelineColorBlendStateCreateInfo::default().attachments(slice::from_ref(&blending)); + let dynamic_states = [DynamicState::VIEWPORT, DynamicState::SCISSOR]; + let dynamic_state = + PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states); + let viewport_state = PipelineViewportStateCreateInfo::default() + .viewport_count(1) + .scissor_count(1); + let mut pipeline_rendering_create_info = PipelineRenderingCreateInfo::default() + .color_attachment_formats(slice::from_ref(&format)); + let create_info = GraphicsPipelineCreateInfo::default() + .push_next(&mut pipeline_rendering_create_info) + .stages(&stages) + .input_assembly_state(&input_assembly_state) + .vertex_input_state(&vertex_input_state) + .rasterization_state(&rasterization_state) + .multisample_state(&multisampling_state) + .color_blend_state(&color_blend_state) + .dynamic_state(&dynamic_state) + .viewport_state(&viewport_state) + .layout(pipeline_layout); + let pipelines = unsafe { + dev.create_graphics_pipelines( + PipelineCache::null(), + slice::from_ref(&create_info), + None, + ) + }; + let mut pipelines = pipelines + .map_err(|e| e.1) + .map_err(VulkanError::CreatePipeline)?; + let pipeline = pipelines.pop().unwrap(); + destroy_layout.forget(); + Ok(Rc::new(VulkanPipeline { + vert: self.blur_vert_shader.clone(), + _frag: frag.clone(), + pipeline_layout, + pipeline, + _descriptor_set_layouts: { + let mut v = ArrayVec::new(); + v.push(self.blur_descriptor_set_layout.clone()); + v + }, + })) + } + pub(super) fn allocate_point(&self) -> u64 { self.last_point.fetch_add(1) + 1 } @@ -1263,6 +1411,7 @@ impl VulkanRenderer { acquire_sync: Some(ct.acquire_sync.clone()), release_sync: ct.release_sync, alpha: ct.alpha.unwrap_or_default(), + discard_alpha: ct.discard_alpha.unwrap_or(-1.0), source_type, copy_type, alpha_mode: ct.alpha_mode, @@ -1361,6 +1510,7 @@ impl VulkanRenderer { acquire_sync: Some(ct.acquire_sync.clone()), release_sync: ct.release_sync, alpha: ct.alpha.unwrap_or_default(), + discard_alpha: ct.discard_alpha.unwrap_or(-1.0), source_type, copy_type, alpha_mode: ct.alpha_mode, @@ -1376,9 +1526,6 @@ impl VulkanRenderer { } } GfxApiOpt::BlurBackdrop(b) => { - // Flush all pending ops in original order, then push a - // barrier op to FrameBuffer pass that will end + restart - // the render pass to do the blur work in between. sync(memory); let mask = if let Some(m) = &b.mask { let tex = m.texture.clone().into_vk(&self.device.device)?; @@ -1393,15 +1540,35 @@ impl VulkanRenderer { tex, source: m.source, threshold: m.threshold, + buffer_resv: m.buffer_resv.clone(), + acquire_sync: Some(m.acquire_sync.clone()), + release_sync: m.release_sync, }) } } else { None }; - memory.ops[RenderPass::FrameBuffer].push(VulkanOp::BlurBarrier(VulkanBlurOp { + // Route to whichever pass actually contains the scene. + // The BlendBuffer holds the linearly-composed scene + // (background + workspace + translucent layers) and is + // copied into the FrameBuffer only at the end of the FB + // pass. Reading FB before that copy would sample an + // empty target and produce a black blur. If BB has been + // elided (no blended content this frame), fall back to + // FB which then carries the full scene itself. + let target_pass = if !memory.paint_regions[RenderPass::BlendBuffer].is_empty() { + RenderPass::BlendBuffer + } else { + RenderPass::FrameBuffer + }; + memory.ops[target_pass].push(VulkanOp::BlurBarrier(VulkanBlurOp { rect: b.rect, passes: b.passes, + offset: b.offset, mask, + cache: b.cache.clone(), + cache_epoch: b.cache_epoch, + cache_pixel_rect: b.cache_pixel_rect, })); } } @@ -1586,7 +1753,7 @@ impl VulkanRenderer { release_sync, }); } else if let VulkanOp::BlurBarrier(b) = cmd - && let Some(m) = &b.mask + && let Some(m) = &mut b.mask { let tex = &m.tex; if tex.execution_version.replace(execution) != execution { @@ -1598,6 +1765,12 @@ impl VulkanRenderer { if let VulkanImageMemory::DmaBuf(_) = &tex.ty { memory.dmabuf_sample.push(tex.clone()) } + memory.textures.push(UsedTexture { + tex: tex.clone(), + resv: m.buffer_resv.take(), + acquire_sync: m.acquire_sync.take().unwrap(), + release_sync: m.release_sync, + }); } } } @@ -1929,6 +2102,7 @@ impl VulkanRenderer { let push = TexPushConstants { vertices: c.range_address, alpha: c.alpha, + discard_threshold: c.discard_alpha, }; unsafe { db.cmd_set_descriptor_buffer_offsets( @@ -1966,6 +2140,7 @@ impl VulkanRenderer { pos, tex_pos, alpha: c.alpha, + discard_threshold: c.discard_alpha, }; unsafe { dev.cmd_push_constants( @@ -2045,6 +2220,7 @@ impl VulkanRenderer { let push = RoundedTexPushConstants { vertices: c.range_address, alpha: c.alpha, + discard_threshold: c.discard_alpha, size_x: c.size[0], size_y: c.size[1], corner_radius_tl: c.corner_radius[0], @@ -2088,6 +2264,7 @@ impl VulkanRenderer { pos: c.target, tex_pos: c.source, alpha: c.alpha, + discard_threshold: c.discard_alpha, size_x: c.size[0], size_y: c.size[1], corner_radius_tl: c.corner_radius[0], @@ -2109,10 +2286,8 @@ impl VulkanRenderer { } } VulkanOp::BlurBarrier(blur) => { - // Blur is only meaningful in the FrameBuffer pass. - if pass != RenderPass::FrameBuffer { - continue; - } + // BlurBarrier is pushed to exactly one pass in convert_ops + // (BB if present, else FB), so no per-pass gating is needed. // End the current dynamic render pass, run the blur work // (image-blit cascade between scratch images), and resume // the render pass with LOAD so subsequent draws layer on @@ -2132,14 +2307,55 @@ impl VulkanRenderer { threshold: m.threshold, _phantom: std::marker::PhantomData, }); + + // Cache lookup: a hit lets us skip the entire blur cascade. + // Only masked blurs are cached. The masked path leaves the + // blurred scratch image in SHADER_READ_ONLY_OPTIMAL, which + // is the layout required by the cache-hit composite path. + let cached_blur: Option> = mask_record + .as_ref() + .and_then(|_| blur.cache.as_ref()) + .and_then(|slot| { + let slot_borrow = slot.borrow(); + slot_borrow.as_ref().and_then(|entry| { + if entry.epoch == blur.cache_epoch + && entry.passes == blur.passes + && entry.offset == blur.offset + && entry.pixel_rect == blur.cache_pixel_rect + { + entry.image.clone().into_vk(&self.device.device).ok() + } else { + None + } + }) + }); + + let mut produced_blur: Option> = None; self.record_blur( buf, target, rect_arr, blur.passes, + blur.offset, &mut local_blur_scratch, mask_record.as_ref(), + cached_blur.as_ref(), + &mut produced_blur, )?; + + // On a masked cache miss, store the freshly-blurred image + // for the next frame to reuse. + if let (Some(_), Some(slot), Some(image)) = + (mask_record.as_ref(), blur.cache.as_ref(), produced_blur) + { + *slot.borrow_mut() = Some(crate::gfx_api::BlurCacheEntry { + pixel_rect: blur.cache_pixel_rect, + passes: blur.passes, + offset: blur.offset, + epoch: blur.cache_epoch, + image, + }); + } self.begin_rendering_load(buf, target); // Pipeline state is invalidated across the render-pass // break — force re-bind on next draw. @@ -2679,6 +2895,7 @@ impl VulkanRenderer { let width = fb.width as f32; let height = fb.height as f32; let mut tag = 0; + let mut blur_rects: Vec = Vec::new(); for opt in opts.iter().rev() { let (opaque, fb_rect) = match opt { GfxApiOpt::Sync => continue, @@ -2706,7 +2923,10 @@ impl VulkanRenderer { (false, rf.rect) } GfxApiOpt::RoundedCopyTexture(ct) => (false, ct.target), - GfxApiOpt::BlurBackdrop(_) => continue, + GfxApiOpt::BlurBackdrop(b) => { + blur_rects.push(b.rect.to_rect(width, height)); + continue; + } }; if opaque || bb.is_none() { tag |= 1; @@ -2719,6 +2939,20 @@ impl VulkanRenderer { } memory.regions_2.push(rect.with_tag(tag)); } + // Force blur source rects into the effective damage region. The blur + // cascade reads its source from BB (or FB), and both buffers persist + // their contents across frames in undamaged regions. Without this, + // a cache-miss cascade can sample a stale composite — including the + // blur surface's own previously-drawn body — and re-blur it, + // producing visible double-shadow artifacts at the blur boundary. + let blur_region_owned = Region::from_rects2(&blur_rects); + let expanded_region; + let region: &Region = if blur_region_owned.is_empty() { + region + } else { + expanded_region = region.union_cow(&blur_region_owned).into_owned(); + &expanded_region + }; let clear_region = if clear.is_some() { let opaque_region = Region::from_rects2(&memory.regions_1); region.subtract_cow(&opaque_region) diff --git a/src/gfx_apis/vulkan/shaders.rs b/src/gfx_apis/vulkan/shaders.rs index 607fd49a..a1894335 100644 --- a/src/gfx_apis/vulkan/shaders.rs +++ b/src/gfx_apis/vulkan/shaders.rs @@ -21,6 +21,9 @@ pub const ROUNDED_TEX_VERT: &[u8] = include_bytes!("shaders_bin/rounded_tex.vert pub const ROUNDED_TEX_FRAG: &[u8] = include_bytes!("shaders_bin/rounded_tex.frag.spv"); pub const BLUR_COMPOSITE_VERT: &[u8] = include_bytes!("shaders_bin/blur_composite.vert.spv"); pub const BLUR_COMPOSITE_FRAG: &[u8] = include_bytes!("shaders_bin/blur_composite.frag.spv"); +pub const BLUR_VERT: &[u8] = include_bytes!("shaders_bin/blur.vert.spv"); +pub const BLUR_DOWN_FRAG: &[u8] = include_bytes!("shaders_bin/blur_down.frag.spv"); +pub const BLUR_UP_FRAG: &[u8] = include_bytes!("shaders_bin/blur_up.frag.spv"); pub const LEGACY_ROUNDED_FILL_VERT: &[u8] = include_bytes!("shaders_bin/legacy_rounded_fill.vert.spv"); pub const LEGACY_ROUNDED_FILL_FRAG: &[u8] = @@ -69,6 +72,7 @@ unsafe impl Packed for TexVertex {} pub struct TexPushConstants { pub vertices: DeviceAddress, pub alpha: f32, + pub discard_threshold: f32, } unsafe impl Packed for TexPushConstants {} @@ -109,6 +113,7 @@ pub struct LegacyTexPushConstants { pub pos: [[f32; 2]; 4], pub tex_pos: [[f32; 2]; 4], pub alpha: f32, + pub discard_threshold: f32, } unsafe impl Packed for LegacyTexPushConstants {} @@ -148,6 +153,7 @@ unsafe impl Packed for LegacyRoundedFillPushConstants {} pub struct RoundedTexPushConstants { pub vertices: DeviceAddress, pub alpha: f32, + pub discard_threshold: f32, pub size_x: f32, pub size_y: f32, pub corner_radius_tl: f32, @@ -165,6 +171,7 @@ pub struct LegacyRoundedTexPushConstants { pub pos: [[f32; 2]; 4], pub tex_pos: [[f32; 2]; 4], pub alpha: f32, + pub discard_threshold: f32, pub size_x: f32, pub size_y: f32, pub corner_radius_tl: f32, @@ -195,6 +202,15 @@ pub struct BlurCompositePushConstants { unsafe impl Packed for BlurCompositePushConstants {} +#[derive(Copy, Clone, Debug)] +#[repr(C)] +pub struct BlurPushConstants { + pub halfpixel: [f32; 2], + pub offset: f32, +} + +unsafe impl Packed for BlurPushConstants {} + impl VulkanDevice { pub(super) fn create_shader( self: &Rc, diff --git a/src/gfx_apis/vulkan/shaders/blur.vert b/src/gfx_apis/vulkan/shaders/blur.vert new file mode 100644 index 00000000..7e0fde54 --- /dev/null +++ b/src/gfx_apis/vulkan/shaders/blur.vert @@ -0,0 +1,26 @@ +#version 450 + +layout(location = 0) out vec2 v_texcoord; + +void main() { + vec2 pos; + switch (gl_VertexIndex) { + case 0: + pos = vec2( 1.0, -1.0); + v_texcoord = vec2(1.0, 0.0); + break; + case 1: + pos = vec2(-1.0, -1.0); + v_texcoord = vec2(0.0, 0.0); + break; + case 2: + pos = vec2( 1.0, 1.0); + v_texcoord = vec2(1.0, 1.0); + break; + case 3: + pos = vec2(-1.0, 1.0); + v_texcoord = vec2(0.0, 1.0); + break; + } + gl_Position = vec4(pos, 0.0, 1.0); +} diff --git a/src/gfx_apis/vulkan/shaders/blur_down.frag b/src/gfx_apis/vulkan/shaders/blur_down.frag new file mode 100644 index 00000000..8afb493a --- /dev/null +++ b/src/gfx_apis/vulkan/shaders/blur_down.frag @@ -0,0 +1,21 @@ +#version 450 + +layout(set = 0, binding = 0) uniform sampler2D tex; + +layout(push_constant, std430) uniform Data { + vec2 halfpixel; + float offset; +} data; + +layout(location = 0) in vec2 v_texcoord; +layout(location = 0) out vec4 out_color; + +void main() { + vec2 hp = data.halfpixel * data.offset; + vec4 sum = textureLod(tex, v_texcoord, 0.0) * 4.0; + sum += textureLod(tex, v_texcoord - hp, 0.0); + sum += textureLod(tex, v_texcoord + hp, 0.0); + sum += textureLod(tex, v_texcoord + vec2(hp.x, -hp.y), 0.0); + sum += textureLod(tex, v_texcoord - vec2(hp.x, -hp.y), 0.0); + out_color = sum / 8.0; +} diff --git a/src/gfx_apis/vulkan/shaders/blur_up.frag b/src/gfx_apis/vulkan/shaders/blur_up.frag new file mode 100644 index 00000000..2397d687 --- /dev/null +++ b/src/gfx_apis/vulkan/shaders/blur_up.frag @@ -0,0 +1,24 @@ +#version 450 + +layout(set = 0, binding = 0) uniform sampler2D tex; + +layout(push_constant, std430) uniform Data { + vec2 halfpixel; + float offset; +} data; + +layout(location = 0) in vec2 v_texcoord; +layout(location = 0) out vec4 out_color; + +void main() { + vec2 hp = data.halfpixel * data.offset; + vec4 sum = textureLod(tex, v_texcoord + vec2(-hp.x * 2.0, 0.0), 0.0); + sum += textureLod(tex, v_texcoord + vec2(-hp.x, hp.y), 0.0) * 2.0; + sum += textureLod(tex, v_texcoord + vec2(0.0, hp.y * 2.0), 0.0); + sum += textureLod(tex, v_texcoord + vec2(hp.x, hp.y), 0.0) * 2.0; + sum += textureLod(tex, v_texcoord + vec2(hp.x * 2.0, 0.0), 0.0); + sum += textureLod(tex, v_texcoord + vec2(hp.x, -hp.y), 0.0) * 2.0; + sum += textureLod(tex, v_texcoord + vec2(0.0, -hp.y * 2.0), 0.0); + sum += textureLod(tex, v_texcoord + vec2(-hp.x, -hp.y), 0.0) * 2.0; + out_color = sum / 12.0; +} diff --git a/src/gfx_apis/vulkan/shaders_bin/blur.vert.spv b/src/gfx_apis/vulkan/shaders_bin/blur.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..6b57ef62c46d2f15f9ed551259216658eee46ba7 GIT binary patch literal 1412 zcmZQ(Qf6mhU}WHC;AJpkfB-=TCI&_Z1_o{hHZbk(6YQf`T#}+^Vrl?V!N;uK3j+f~ZenI0hyxN4XJBB^VqjokW?*JuVqjo6#K^$F!obDg?i26s@9*y88lPNH z5MNwUnUflylbM$qpORUWnp~1umI_kGfut@oFFB_)1z7kki4RCE zf@lS~pN#<`4q}7k@{2P|GV}8o*cjNs>Oo-x7SGHp3C^ra1*wC{f&7}D6YrdpS>Tdc zT#}fVoC?=av(_t1_pNqX0ZJrJ3JUz z!EBHeNIi&#iGkb)5(CjNF_0dR7|0DUF%TOh1`11I1}1PkDS~5+0n8R*S z0HqKR&BDMAWrM`686bR6e1q%~V_;_BhpHE6U||qoU;y($b}KNjFbFa*fc1mKwHa8z za>5{oGc?%4_#zPgejNr@uzpbn1_qECkT}Q;F>sn<0Ly{w2k}8+1o8{a3<4KLK>Coy6rp-SVj#b(K>ZF1 zTTnQvLc;~*G>|(%d^KoT$}q4ns6*9*#N?pq4`v2P4unDe*M#~Xlx{)(2k}Aa2j+iJ zyl8_g0o%vI0Lri+IgnXq46NXI1-S{N1SI~0fr){Yfq?;J7ASr|`2`fmw$QleWMBZN z$?puz416F>3=9mv8Mqlh;xIlJ)E}U<1d3}Os2h117#Kk2gXHa?c@Y$!AT}t@K=JaE zfeCCbNF7K&NDkx%Q2c<{Aa$U4K~@JcA0!7-2Z|pM8>9}zMpg&1A0!7-2Z}Qg8>9{t zSIFu>{s8$GlrD7`7{FylAOkBned;oR(haQBIK_xE@A zag9$dD2OjEsmw`@&&kY7jZeueN=+`wEK3Ec<3LiEnU|bXnu4r=m4O9pKFnTL24)6M z1_p+V0tN<<92WxvgG*vbBFIf_3~US_K8T%>n3GnJS&^E+2*zftJ zCGpAmIr&8l3=HlJ%wRh}{`Y{|2Xcoph+tq~fQf>tSI4*`o+moVU|?YY*$WZ_VL1jC zu$w__kQ$IYHvVn^+ixq4FU6K<*O<#}6nSK;$JD7{G1@iAyqYGDtz?LH0qd0aGx(Gy^k(Gy?-z z9msweuq*>u4CHSm z4-x{&gZSnk4p^Lp!2;@jkU21UYp9(d`vbuFgnxecTqq%Rm^o}(E9 zD>zO;c@HE6QuBj>iGh`YfdQl!6qlfQf%yd#cAzv2;yW_1GJxVjfPsMl6n~&NvW2Dx zL2$lhU;wEB#fJz~4Je*KYCvufWnf?ciT5$Eg5yJsfq?;}48s*01^Y~fw>1H22u}lk1m>f^w8X+4|Na7Jdk@q1u94lNF3%KLk0#0kU1bZ zboUscxyP7+fdM22(gSl3NDSs46EydjLfr$3Ur>6qgt`ZmPCbbzop%0ND?cgZb5wfq?de5v01^Y~f%z3A267W9>|N0O>Wb!9H>f|5{p!xZzyK15`PBoe2HCHk z;4+$l0p?dP1_lO@7|gHU;PRS*0VEDfXFd!J3?TbKaxlO8g6bm%1_qEEx?la!{OZrZ zzyJ~h>4EtbBnI*;C|w3Y)dxWHPzVD91E_33#vRZ!8_K}I08$T912PL#rhvr$GB7e& IFfcLz01LHx-~a#s literal 0 HcmV?d00001 diff --git a/src/gfx_apis/vulkan/shaders_bin/blur_up.frag.spv b/src/gfx_apis/vulkan/shaders_bin/blur_up.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..56b6fe73b0e7555ab97ca7ca911c5d8368464298 GIT binary patch literal 3036 zcmZQ(Qf6mhU}WHC;AQAwfB-=TCI&_Z1_o{hHZbk(6YQf`T#}+^Vrl?V!NaQBIK_xE@A zag9$dD2OjEsmw`@&&kY7jZeueN=+`wEK3Ec<3LiEnU|bXnu4r=m4O9pKFnTL24)6M z1_p+V0tN<<92WxvgG*vbBFIf_3~US_K8T%>n3GnJS&^E+2*zftJ zCGpAmIr&8l3=HlJ%wRh}{`Y{|2Xcoph+tq~fQf>tSI4*`o+moVU|?YY*$WZ_VL1jC zu$w__kQ$IYHvVn^+ixq4FU6K<*O<#}6nSK;$JD7{G1@iAyqYGDtz?LH0qd0aGx(Gy^k(Gy?-z z9msweuq*>u4CHk;J;?naJ}ACGe2{(> zsQWrM*_#pkpP&+~To51;mfdS+{ko_P&C{97S4Wu4qUMpC? zgQFP(D>zO;c@HE6QuBj>iGh`YfdQl!6qlfQf%yd#cAzv2;yW_1GJxVjfPsMl6n~&N zvW2DxL2$lhU;wEB#fJz~4Je*KYCvufWnf?ciT5$Eg5yJsfq?;}4S?B-YBn3g&Av zFff3`K;atzb%!p^ho*0syFrB^D2$QaZOFjD01}6}+lYaI0VD>~V*>RnDBeNl zg8XXAz`y_!hxx;dfq?;}4^fq?;JK1dGcKW7F829O&-axnja+y-*H3z}bC zL1jGy0|Q78%r77@kb00`+|c~uj^-B+s9%ud(i2=ZL-GPBoq9plAjh3I0|Ns{9OgG4 z1_lO@K9D%fZ@%F2oPhx(2lJaB0|NudO&~d#-(Yd)&%nR{k_Uwu$o(My2B7&jkb!{# zBnDCo^DjsYq#oqoAT<95qxm-k>KA1HhB7cPfW%>O7skNA01^Ya1LnVQ1_lO@I4J&L z{)=E>U;vp9l7sm#l7WE%?}Ma@>I$1|V@*UQJ?PU;v51{F=r=#6a$X`8OY$FF|rJ{}wPXFo5g>$-(?v$iTn= zauY}n=3kIoL4E_ZcaY1NVg?2VkQhi0%x@qukb02cO3?gPisrX6sNay&X*mM}14ta^ zw+aRZ29Ovmt}CH>kmI_Ffq?-e4%1W3z`y_!gZaINfq?;J4oDp4_gV%929SF|axlNw zf!cly3=AMSnBQS>UC+S40FnoVDae1I@Mu7b>qZ6!29Ov?Ei61hVleeBQ1t=Ox~h$V nfdN$KBjXMRR&YJm&cMI`QV&uCG7HpJ0Ezu&U}UghU}OLQ&Gy2b literal 0 HcmV?d00001 diff --git a/src/gfx_apis/vulkan/shaders_hash.txt b/src/gfx_apis/vulkan/shaders_hash.txt index de378df5..2fde0603 100644 --- a/src/gfx_apis/vulkan/shaders_hash.txt +++ b/src/gfx_apis/vulkan/shaders_hash.txt @@ -1,6 +1,9 @@ 302a9f250bdc4f8e0e71a9f77c9a8a7aa55fd003bc91c2422a700c4abd83f54e src/gfx_apis/vulkan/shaders/alpha_modes.glsl +65acbe7a6496279fa22f520ad2036d3e14a7cb1707c6a509ce7858adc4a2dcba src/gfx_apis/vulkan/shaders/blur.vert 16ad6f1eb029ccce5e0204a7d79709b05a8a708133feaf8bb20a24371de25ed7 src/gfx_apis/vulkan/shaders/blur_composite.frag 6399e23afa2e07c98b9fd1a4e853ea974a9958547ce65734846483bd7cbc8461 src/gfx_apis/vulkan/shaders/blur_composite.vert +a04b2453c39efb018754fc25d45a369b5813359c55fad1c99020804cbb3a18e0 src/gfx_apis/vulkan/shaders/blur_down.frag +f6d51f3b5410387d1474529c44e71bfdc31ceb80174ea6e3e4c2df30d03f11c3 src/gfx_apis/vulkan/shaders/blur_up.frag b6a0df1e231fab533499329636b7a580384784418baee06c147af5fcc384cf5c src/gfx_apis/vulkan/shaders/eotfs.glsl 8a38df18851cd13884499820f26939fb7319f45d913d867f254d8118d59fb117 src/gfx_apis/vulkan/shaders/fill.common.glsl 21c488d12aa5ad2f109ec44cb856dfe837e02ea9025b5ed64439d742c17cbf30 src/gfx_apis/vulkan/shaders/fill.frag From bb43c238e37a94967a31db8e9d4517e658a9d3e4 Mon Sep 17 00:00:00 2001 From: entailz Date: Wed, 20 May 2026 18:47:16 -0700 Subject: [PATCH 2/4] Adds a discard_threshold push-constant to the tex / rounded_tex shaders --- .../shaders/legacy/rounded_tex.common.glsl | 15 ++++++++------- .../vulkan/shaders/legacy/rounded_tex.frag | 3 +++ .../vulkan/shaders/legacy/tex.common.glsl | 1 + src/gfx_apis/vulkan/shaders/legacy/tex.frag | 3 +++ .../vulkan/shaders/rounded_tex.common.glsl | 15 ++++++++------- src/gfx_apis/vulkan/shaders/rounded_tex.frag | 3 +++ src/gfx_apis/vulkan/shaders/tex.common.glsl | 1 + src/gfx_apis/vulkan/shaders/tex.frag | 3 +++ .../shaders_bin/legacy_rounded_tex.frag.spv | Bin 6648 -> 6860 bytes .../shaders_bin/legacy_rounded_tex.vert.spv | Bin 2804 -> 2876 bytes .../vulkan/shaders_bin/legacy_tex.frag.spv | Bin 1852 -> 2096 bytes .../vulkan/shaders_bin/legacy_tex.vert.spv | Bin 1924 -> 1980 bytes .../vulkan/shaders_bin/rounded_tex.frag.spv | Bin 19700 -> 19912 bytes .../vulkan/shaders_bin/rounded_tex.vert.spv | Bin 3580 -> 3652 bytes src/gfx_apis/vulkan/shaders_bin/tex.frag.spv | Bin 15032 -> 15228 bytes src/gfx_apis/vulkan/shaders_bin/tex.vert.spv | Bin 2572 -> 2628 bytes src/gfx_apis/vulkan/shaders_hash.txt | 16 ++++++++-------- 17 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/gfx_apis/vulkan/shaders/legacy/rounded_tex.common.glsl b/src/gfx_apis/vulkan/shaders/legacy/rounded_tex.common.glsl index f221ff6a..1e95d980 100644 --- a/src/gfx_apis/vulkan/shaders/legacy/rounded_tex.common.glsl +++ b/src/gfx_apis/vulkan/shaders/legacy/rounded_tex.common.glsl @@ -2,11 +2,12 @@ layout(push_constant, std430) uniform Data { layout(offset = 0) vec2 pos[4]; layout(offset = 32) vec2 tex_pos[4]; layout(offset = 64) float mul; - layout(offset = 68) float size_x; - layout(offset = 72) float size_y; - layout(offset = 76) float corner_radius_tl; - layout(offset = 80) float corner_radius_tr; - layout(offset = 84) float corner_radius_br; - layout(offset = 88) float corner_radius_bl; - layout(offset = 92) float scale; + layout(offset = 68) float discard_threshold; + layout(offset = 72) float size_x; + layout(offset = 76) float size_y; + layout(offset = 80) float corner_radius_tl; + layout(offset = 84) float corner_radius_tr; + layout(offset = 88) float corner_radius_br; + layout(offset = 92) float corner_radius_bl; + layout(offset = 96) float scale; } data; diff --git a/src/gfx_apis/vulkan/shaders/legacy/rounded_tex.frag b/src/gfx_apis/vulkan/shaders/legacy/rounded_tex.frag index cec6f001..8c84529f 100644 --- a/src/gfx_apis/vulkan/shaders/legacy/rounded_tex.frag +++ b/src/gfx_apis/vulkan/shaders/legacy/rounded_tex.frag @@ -38,6 +38,9 @@ void main() { vec2 size = vec2(data.size_x, data.size_y); vec4 corner_radius = vec4(data.corner_radius_tl, data.corner_radius_tr, data.corner_radius_br, data.corner_radius_bl); vec4 c = textureLod(tex, tex_pos, 0); + if (c.a < data.discard_threshold) { + discard; + } if (has_alpha_multiplier) { if (src_has_alpha) { c *= data.mul; diff --git a/src/gfx_apis/vulkan/shaders/legacy/tex.common.glsl b/src/gfx_apis/vulkan/shaders/legacy/tex.common.glsl index 61f3ef3c..82ec59ba 100644 --- a/src/gfx_apis/vulkan/shaders/legacy/tex.common.glsl +++ b/src/gfx_apis/vulkan/shaders/legacy/tex.common.glsl @@ -2,4 +2,5 @@ layout(push_constant, std430) uniform Data { layout(offset = 0) vec2 pos[4]; layout(offset = 32) vec2 tex_pos[4]; layout(offset = 64) float mul; + layout(offset = 68) float discard_threshold; } data; diff --git a/src/gfx_apis/vulkan/shaders/legacy/tex.frag b/src/gfx_apis/vulkan/shaders/legacy/tex.frag index 2896bc87..ab862b32 100644 --- a/src/gfx_apis/vulkan/shaders/legacy/tex.frag +++ b/src/gfx_apis/vulkan/shaders/legacy/tex.frag @@ -9,6 +9,9 @@ layout(location = 0) out vec4 out_color; void main() { vec4 c = textureLod(tex, tex_pos, 0); + if (c.a < data.discard_threshold) { + discard; + } if (has_alpha_multiplier) { if (src_has_alpha) { c *= data.mul; diff --git a/src/gfx_apis/vulkan/shaders/rounded_tex.common.glsl b/src/gfx_apis/vulkan/shaders/rounded_tex.common.glsl index 9a07f0b0..62c11e74 100644 --- a/src/gfx_apis/vulkan/shaders/rounded_tex.common.glsl +++ b/src/gfx_apis/vulkan/shaders/rounded_tex.common.glsl @@ -12,11 +12,12 @@ layout(buffer_reference, buffer_reference_align = 8, std430) readonly buffer Ver layout(push_constant, std430) uniform Data { layout(offset = 0) Vertices vertices; layout(offset = 8) float mul; - layout(offset = 12) float size_x; - layout(offset = 16) float size_y; - layout(offset = 20) float corner_radius_tl; - layout(offset = 24) float corner_radius_tr; - layout(offset = 28) float corner_radius_br; - layout(offset = 32) float corner_radius_bl; - layout(offset = 36) float scale; + layout(offset = 12) float discard_threshold; + layout(offset = 16) float size_x; + layout(offset = 20) float size_y; + layout(offset = 24) float corner_radius_tl; + layout(offset = 28) float corner_radius_tr; + layout(offset = 32) float corner_radius_br; + layout(offset = 36) float corner_radius_bl; + layout(offset = 40) float scale; } data; diff --git a/src/gfx_apis/vulkan/shaders/rounded_tex.frag b/src/gfx_apis/vulkan/shaders/rounded_tex.frag index 0c506bc2..aebbdb32 100644 --- a/src/gfx_apis/vulkan/shaders/rounded_tex.frag +++ b/src/gfx_apis/vulkan/shaders/rounded_tex.frag @@ -45,6 +45,9 @@ void main() { vec2 size = vec2(data.size_x, data.size_y); vec4 corner_radius = vec4(data.corner_radius_tl, data.corner_radius_tr, data.corner_radius_br, data.corner_radius_bl); vec4 c = textureLod(sampler2D(tex, sam), tex_pos, 0); + if (c.a < data.discard_threshold) { + discard; + } if (eotf != inv_eotf || has_matrix || alpha_mode != AM_PREMULTIPLIED_ELECTRICAL) { vec3 rgb = c.rgb; if (src_has_alpha && alpha_mode == AM_PREMULTIPLIED_ELECTRICAL) { diff --git a/src/gfx_apis/vulkan/shaders/tex.common.glsl b/src/gfx_apis/vulkan/shaders/tex.common.glsl index 8576231b..45b7119a 100644 --- a/src/gfx_apis/vulkan/shaders/tex.common.glsl +++ b/src/gfx_apis/vulkan/shaders/tex.common.glsl @@ -12,4 +12,5 @@ layout(buffer_reference, buffer_reference_align = 8, std430) readonly buffer Ver layout(push_constant, std430) uniform Data { Vertices vertices; float mul; + float discard_threshold; } data; diff --git a/src/gfx_apis/vulkan/shaders/tex.frag b/src/gfx_apis/vulkan/shaders/tex.frag index e1ccacda..2e09c32c 100644 --- a/src/gfx_apis/vulkan/shaders/tex.frag +++ b/src/gfx_apis/vulkan/shaders/tex.frag @@ -16,6 +16,9 @@ layout(location = 0) out vec4 out_color; void main() { vec4 c = textureLod(sampler2D(tex, sam), tex_pos, 0); + if (c.a < data.discard_threshold) { + discard; + } if (eotf != inv_eotf || has_matrix || alpha_mode != AM_PREMULTIPLIED_ELECTRICAL) { vec3 rgb = c.rgb; if (src_has_alpha && alpha_mode == AM_PREMULTIPLIED_ELECTRICAL) { diff --git a/src/gfx_apis/vulkan/shaders_bin/legacy_rounded_tex.frag.spv b/src/gfx_apis/vulkan/shaders_bin/legacy_rounded_tex.frag.spv index 9fd78f9b1dae52aaa046ef054e720e1f65c74942..300db829a22f4d8ea1c0b1e47a9a95e8ee48f3d8 100644 GIT binary patch delta 1600 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 literal 1852 zcmZQ(Qf6mhU}WHC;AOC5fB-=TCI&_Z1_o{hHZbk(6YQf`T#}+^Vrl?V!NaQBIK_xE@A zag9$dD2OjEsmw`@&&kY7jZeueN=+`wEK3Ec<3LiEnU|bXnu4r=m4O9pKFnTL24)6M z1_p*?1_lNYo0oxsp(M3}0i+J(rjpc(_=5am237_R1~CQ(hK$7G_{5xojKuid(wvga zf}G6MB9Qx88Q2&k85kIfi<0BvYQSc)FeorEFt{X^B!bkjF+lhrHppBy23D{*mj1_p-A zys~&?F&n5KauZ96GAlrSW@WHtU|@jyDL*BZfx(@Dg#i=}iXej+5PXmuKtdpSP}s|*ns>1!XSB2Sb*3dB_KXXEgJ&^0|!*SJOcv*g3k;N1CW0`py2`1rwk$(7#Lt; zAoDJ48jad;Pj^mPKgX)wip95*xevEh%>M-fYK?*A0Yh-3@l*(fy6*ql7W+f3+hjh znIJd8FpMwFz|6qSzyMYU($51`01~riU||69L2&^R2HD3CH4o$;ka__IP)=lE0EvUt zgZQAh0hzDOzyfxwFi0~41A_0P{H-h+}v;-0Z*$Ik2 zkQ~g-AifF%B)mcTRT&t-c@$(GC=9frVE~Flki0p#tY84g1;~65ACwM2@rGVcci69Xu1fb0RKWso00ac#@M0**&M z1_p560r5fMBE-PJ0E&+Y26nLfL>L$tKx#nZpg07@jUBWM0?B=6U}lg2S;oM?@QZ;3 zT;_-~Fo4BCc7xa;HPT3GKyfFy z3=AM~5Fg}r4QTlGF|dN&sL8;<01^YKgSibP_LG5$0c0=84v<|i{W?(n{tO%ppt#Xx zU|;}=fiTFKpm+uO73L;AEN;?gU|;}=gWLcz15{~%%mInR^ciB&X9P~q3=Dw`pc0sY z!I*)80i+jXFHFn?Dh6@~Ow1H2269&bG;YioAo@Y-LGmE8Vg3inA^RT`4j?`#u0Y`d aqG91-2@MaBUQjv&@&7U~GFUJ$G5`R7vV+qA diff --git a/src/gfx_apis/vulkan/shaders_bin/legacy_tex.vert.spv b/src/gfx_apis/vulkan/shaders_bin/legacy_tex.vert.spv index edf86be6cb7970c8b0cdb14e9c7b1849d474572b..7361189a9ba7fbe35f16840989025e94add16eb7 100644 GIT binary patch delta 88 zcmZqS-^0H_hf$u5frCMufq{XUfq@|Yqb8%E2Lmfy hkun1VgUjYXCPhYmHn5;50|SE?0|Nv9=2~WU769}A4m|(> delta 34 pcmdnP-@?B^hjFtHqXy&V4kkH9ZdL|y1_lOE1_lPP&0Cq-SpbZx28jRw diff --git a/src/gfx_apis/vulkan/shaders_bin/rounded_tex.frag.spv b/src/gfx_apis/vulkan/shaders_bin/rounded_tex.frag.spv index 2bdd1fd3d7ebba2a94274472943b04e4b3f8b6b2..3ff5c2805930082a7b35669c2b7e92c5b9818de6 100644 GIT binary patch delta 3801 zcmew|lkvoCMqXwnWp)MzMh0#MUWV9-yv3YPnHU)SnHdkxG*NVNFj!7*s4R}rl1_osY1_q7E z7rFcEL7}Axb|eFXI|B;?h_A!|id_gF6oSa|AU;SQqy?lN6smF{4g*9zC{!647#Khi z3z7$gD2UGrl?MeNh!4`@&cJNM016{eTJd0D0kc68Ak84>g5*Gab_NCp0R{#Ju-Ifl zUSUU2D1w{?G74l3h!1iWjBm*d@dAtwN}V8Q!uZjfQ+ZEvF!D^+7t@>^AjUE|LTm~n z=j69y(!R%;AgSQtQjP&k9UCdI(SaF2;0~^C#1_p)?ObiUG85kx%7Z5TrmJssN+`=5kPS0VxKBA}FbVLJ_1kikX1{ zl!rlSTLuPj2?XPVVjom$AoD>51Tr6#?O}W`1_lODhKKR3 z85qEs9GMTwKQ;e0;cF969WTC0;cFPL=gihm%!9qVPaqaiNn-fgQ@}L9GIHxObiSl@c;&P za8%!5VqgF{0AxN$KS&N?`W&qd00Ss(lU$HYYFn|=pa0de`Se%2IfdQlzBoFf;NbDyA6WlW(Pr=OP zf|?6b3(B1!z9q=4dIkmtZe|7sQw9cby$(_U3NDZsD7g4RK86MtsC43EW?%q`fkF$! zhJ}^@mXHu+W?%q`!_)|2QByC>%)kJWfCZNbGXn!i3^}+&nHd;B;%*EK3?M^cz7m7_ z3Km-8%nS@5wJ;0|EeU1@29R2iJj{b2v7Zb~^>E*Sd|b9nl9$nhXpKhRh5MATf|Sn8hG5 zkSAc4fM}S0BdC6;r3}VUJB2|7CoF7Dm>C#A3PF(oG8Ci;BnH!D3e^Ovm|=R%pn5=Y z1kwXi0}_MjF^B5OWMW`|>9K(70mY$R4g)g-ND)X7EK4xHI0I@-i0Tse_lM5Av>+P8t80?rK#W1MAbbvY$ z6t*C~BQpa7NDQnF+N}VIfm{c&31lOt}#vqA9&s?mANM;7Ni$IbfLqN4_I5VWJ1mc4Xjetf0C@ewzNM;5GkQhiE$Rd#MKw>cUF;Mj& PxoBnvhM3K=%GWgkPTR^t delta 3637 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 delta 30 mcmX>i(j&5=hjH^7#tO#G7R-wn*;yF)85kJ&Ha}&FX9WP3FbGHh diff --git a/src/gfx_apis/vulkan/shaders_hash.txt b/src/gfx_apis/vulkan/shaders_hash.txt index 2fde0603..f8b7f0e8 100644 --- a/src/gfx_apis/vulkan/shaders_hash.txt +++ b/src/gfx_apis/vulkan/shaders_hash.txt @@ -15,11 +15,11 @@ ad22a79e1a88a12daa40c0a2b953084c129a408297c8ca544d60e0b6001470b9 src/gfx_apis/vu b77838c0aac9ec90ae76cd0d94d3891d72d9a30b09ce77009afd9f4e567dd042 src/gfx_apis/vulkan/shaders/legacy/rounded_fill.common.glsl fa39734aea1c96960f5dc95b999ae2fa5576ecf4b527fd70ee0f643c8ddcc452 src/gfx_apis/vulkan/shaders/legacy/rounded_fill.frag c1914cc00fb4827f65cd55bd0737d159fe44a098a3085a500822fc91cc2bfcad src/gfx_apis/vulkan/shaders/legacy/rounded_fill.vert -bd249cf170b72cd833e92a7719e88da0a91e563956579707e693679b443d73d5 src/gfx_apis/vulkan/shaders/legacy/rounded_tex.common.glsl -28f3249e0d974a332b2926fb7565930627a093d6ac21ca17f2bf191740d299bd src/gfx_apis/vulkan/shaders/legacy/rounded_tex.frag +0305f0bf2ab87de4280e32adfda21906304db595590baa0f024d4e5e67d80d9c src/gfx_apis/vulkan/shaders/legacy/rounded_tex.common.glsl +02405debc59f254cd95f6b7f94df27438c952b22f357f411359898f430bcd770 src/gfx_apis/vulkan/shaders/legacy/rounded_tex.frag 6ef0bde549dc163cd08f68d975071f5d74213c07ccc4a06b30c6f179b2f848ae src/gfx_apis/vulkan/shaders/legacy/rounded_tex.vert -e0a8769dd7938dd02e66db9e9048ed6bef8f8c42671f2e2c7a7976a6d498f685 src/gfx_apis/vulkan/shaders/legacy/tex.common.glsl -0e7c72ea11671065842c8b4ad4131a7df33b427dc0ea76bf5a896546f6636cb0 src/gfx_apis/vulkan/shaders/legacy/tex.frag +f5bfdb445c501ab97a19c7d435996a03ed45d31e8e54e29143f1daad8fa60d5b src/gfx_apis/vulkan/shaders/legacy/tex.common.glsl +3a9b36f72c82067e1892481054acb0948097d6c766e62e8bfad766fa2c2e3de6 src/gfx_apis/vulkan/shaders/legacy/tex.frag 4402f7ccdbb9fb52fb6cda3aab13cf89e2980c79b541f8be0463efd64a5f98ed src/gfx_apis/vulkan/shaders/legacy/tex.vert 3ba5d05c2b95099e5424b3ade5d1c31d431f5730b1d0b51a9fb5f8afc4ea14b4 src/gfx_apis/vulkan/shaders/out.common.glsl 5069f619c7d722815a022e2d84720a2d8290af49a3ed49ea0cd26b52115cc39a src/gfx_apis/vulkan/shaders/out.frag @@ -27,10 +27,10 @@ e0a8769dd7938dd02e66db9e9048ed6bef8f8c42671f2e2c7a7976a6d498f685 src/gfx_apis/vu 9202d5c9fc4ce0d5f40ed147f245bd037728c9e060ea46a0f0a1767ca55e6c48 src/gfx_apis/vulkan/shaders/rounded_fill.common.glsl 9085625d2afb1365685ae79a58108bf6566573ed94d9913397cf74dc6ef9b6e8 src/gfx_apis/vulkan/shaders/rounded_fill.frag 7665319a706e514f125d80f51f10b643f01cdae54d8a6ea56c218f78de7c0ecb src/gfx_apis/vulkan/shaders/rounded_fill.vert -dd100d048c0b380c913cffd7ac48fed3a341b3cb052302a11c369967f38aba9a src/gfx_apis/vulkan/shaders/rounded_tex.common.glsl -454f34754ea4102190821c2d168dedd8c6bf624f1712b6136d902428f801a1e9 src/gfx_apis/vulkan/shaders/rounded_tex.frag +0fa53622bbee536bdf0b32438c276b9e5231e1fe5fac93ed395426da3893bd74 src/gfx_apis/vulkan/shaders/rounded_tex.common.glsl +adeba99236ee7606170bfb48e62c0df11c71a83018d8a201cde760c4f569fe5e src/gfx_apis/vulkan/shaders/rounded_tex.frag 21b18ba369b505b9aedb8cf2e7e31bc417f6704fd2daac353b0db52f9ae44c70 src/gfx_apis/vulkan/shaders/rounded_tex.vert -e22d4d3318a350def8ef19c7b27dc6a308a84c2fe9d7c02b81107f72073cd481 src/gfx_apis/vulkan/shaders/tex.common.glsl -1f196cee646a934072beb3e5648a5042c035953d9a0c26b0a22e330c2f8bb994 src/gfx_apis/vulkan/shaders/tex.frag +6ebf70abd2a06cb8a14cea7022a19d5d4bc95b1ef5e5a7ca22ab4c5fa37b6244 src/gfx_apis/vulkan/shaders/tex.common.glsl +fdfc60c64a22e7745dc82642ea23ef214dbd3b92d6a4f0ae1d75d33e89ae6a6a src/gfx_apis/vulkan/shaders/tex.frag 423cf327c9fcc4070dbf75321c1224a1589b6cf3d2f1ea5e8bd0362e1a9f3aa1 src/gfx_apis/vulkan/shaders/tex.vert b982f7101c22931a33b32dce3408387f3392c0f0ad0ca5852da265b0d12856bb src/gfx_apis/vulkan/shaders/tex_set.glsl From 12adb678bbe1bec258b55a61ce8bc87f05051bfd Mon Sep 17 00:00:00 2001 From: entailz Date: Wed, 20 May 2026 18:48:48 -0700 Subject: [PATCH 3/4] accepts_input_at rejects buffer-less surfaces --- src/ifs/wl_surface.rs | 50 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index 547b7e2a..e9ae9d7b 100644 --- a/src/ifs/wl_surface.rs +++ b/src/ifs/wl_surface.rs @@ -318,7 +318,7 @@ pub struct WlSurface { pub content_type: Cell>, pub drm_feedback: CopyHashMap>, syncobj_surface: CloneCell>>, - destroyed: Cell, + pub destroyed: Cell, commit_timeline: CommitTimeline, alpha_modifier: CloneCell>>, alpha: Cell>, @@ -1019,6 +1019,7 @@ impl WlSurfaceRequestHandler for WlSurface { self.unset_dnd_icons(); self.unset_cursors(); self.ext.get().on_surface_destroy()?; + self.destroyed.set(true); self.destroy_node(); { let mut children = self.children.borrow_mut(); @@ -1029,6 +1030,19 @@ impl WlSurfaceRequestHandler for WlSurface { } *children = None; } + // Capture a close-animation snapshot if the client is destroying the + // surface while it still has a buffer (i.e. without a clean null-attach + // commit first — typical for crash/disconnect paths). + if self.buffer.is_some() + && let Some(tl) = self.toplevel.get() + && let Some(snap) = crate::animation::capture_snapshot(&self.client.state, &tl) + { + self.client + .state + .close_snapshots + .borrow_mut() + .push(Rc::new(snap)); + } self.buffer.set(None); self.reset_shm_textures(); if let Some(xwayland_serial) = self.xwayland_serial.get() { @@ -1041,7 +1055,6 @@ impl WlSurfaceRequestHandler for WlSurface { self.client.remove_obj(self)?; self.idle_inhibitors.clear(); self.constraints.take(); - self.destroyed.set(true); Ok(()) } @@ -1238,8 +1251,24 @@ impl WlSurface { let mut buffer_changed = false; let mut old_raw_size = None; let (mut dx, mut dy) = mem::take(&mut pending.offset); + let mut buffer_presence_changed = false; if let Some(buffer_change) = pending.buffer.take() { buffer_changed = true; + buffer_presence_changed = buffer_change.is_some() != self.buffer.is_some(); + // If the client just attached a null buffer to the main surface of + // a mapped toplevel, capture a snapshot before we drop the buffer + // so the close animation has something to render after teardown. + if buffer_change.is_none() + && self.buffer.is_some() + && let Some(tl) = self.toplevel.get() + && let Some(snap) = crate::animation::capture_snapshot(&self.client.state, &tl) + { + self.client + .state + .close_snapshots + .borrow_mut() + .push(Rc::new(snap)); + } if let Some(buffer) = self.buffer.take() { old_raw_size = Some(buffer.buffer.buf.rect); } @@ -1408,6 +1437,16 @@ impl WlSurface { }; self.is_opaque.set(is_opaque); } + if buffer_abs_pos_size_changed || buffer_presence_changed { + // Pointer focus depends on whether this surface accepts input. + // It just changed (size shrank/grew, or buffer went from null to + // non-null or vice versa — the latter happens when a client + // dismisses a subsurface by null-attaching while keeping its + // wp_viewport destination). Force a re-evaluation so the pointer + // stack doesn't keep a now-invisible surface focused until the + // next motion event. + self.client.state.tree_changed(); + } let mut tearing_changed = false; if let Some(tearing) = pending.tearing.take() && self.tearing.replace(tearing) != tearing @@ -1597,6 +1636,13 @@ impl WlSurface { } fn accepts_input_at(&self, mut x: i32, mut y: i32) -> bool { + // Per the wayland spec, a surface without a buffer is invisible and + // cannot receive input. Without this check, a client that null-buffers + // but keeps a wp_viewport destination set (as foot does for its + // fractional-scaling subsurfaces) would keep an invisible hit-rect. + if self.buffer.is_none() { + return false; + } let rect = self.buffer_abs_pos.get().at_point(0, 0); if !rect.contains(x, y) { return false; From e35dce433a37109b001e80a302c09007692621cf Mon Sep 17 00:00:00 2001 From: entailz Date: Wed, 20 May 2026 18:50:11 -0700 Subject: [PATCH 4/4] Add open and close animations, xdg_popup blur pre-pass, damage viz config option. --- jay-config/src/_private.rs | 52 +++++ jay-config/src/_private/client.rs | 8 + jay-config/src/_private/ipc.rs | 10 +- jay-config/src/lib.rs | 28 +++ src/animation.rs | 193 ++++++++++++++++ src/compositor.rs | 4 + src/config/handler.rs | 14 ++ src/gfx_api.rs | 34 ++- src/ifs/wl_seat/event_handling.rs | 4 +- src/ifs/wl_surface/xdg_surface/xdg_popup.rs | 72 ++++-- .../wl_surface/xdg_surface/xdg_toplevel.rs | 1 + src/ifs/wl_surface/zwlr_layer_surface_v1.rs | 4 + src/main.rs | 1 + src/renderer.rs | 215 +++++++++++++++++- src/renderer/renderer_base.rs | 71 +++--- src/state.rs | 73 ++++++ src/tree/output.rs | 1 + src/tree/toplevel.rs | 87 +++++++ toml-config/src/config.rs | 26 +++ toml-config/src/config/parsers.rs | 2 + toml-config/src/config/parsers/animations.rs | 73 ++++++ toml-config/src/config/parsers/config.rs | 32 ++- .../config/parsers/damage_visualization.rs | 65 ++++++ toml-config/src/lib.rs | 53 ++++- 24 files changed, 1056 insertions(+), 67 deletions(-) create mode 100644 src/animation.rs create mode 100644 toml-config/src/config/parsers/animations.rs create mode 100644 toml-config/src/config/parsers/damage_visualization.rs diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 9f0a92c5..83aeccad 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -169,3 +169,55 @@ impl Default for BlurConfigIpc { } } } + +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] +pub enum AnimationCurveIpc { + Linear, + EaseOut, + EaseInOut, + /// Standard CSS cubic-bezier(x1, y1, x2, y2). P0=(0,0), P3=(1,1) are fixed. + Bezier { + x1: f32, + y1: f32, + x2: f32, + y2: f32, + }, +} + +#[derive(Serialize, Deserialize, Copy, Clone, Debug)] +pub struct AnimationsConfigIpc { + pub enabled: bool, + pub open_duration_ms: u32, + pub open_curve: AnimationCurveIpc, + pub close_duration_ms: u32, + pub close_curve: AnimationCurveIpc, +} + +impl Default for AnimationsConfigIpc { + fn default() -> Self { + Self { + enabled: false, + open_duration_ms: 200, + open_curve: AnimationCurveIpc::EaseOut, + close_duration_ms: 200, + close_curve: AnimationCurveIpc::EaseOut, + } + } +} + +#[derive(Serialize, Deserialize, Copy, Clone, Debug)] +pub struct DamageVisualizationIpc { + pub enabled: bool, + pub color: crate::theme::Color, + pub decay_millis: u64, +} + +impl Default for DamageVisualizationIpc { + fn default() -> Self { + Self { + enabled: false, + color: crate::theme::Color::new_straight(255, 0, 0, 128), + decay_millis: 2000, + } + } +} diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 2717a3e4..493c09a9 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -880,6 +880,14 @@ impl ConfigClient { self.send(&ClientMessage::SetBlurConfig { config }) } + pub fn set_damage_visualization(&self, config: crate::_private::DamageVisualizationIpc) { + self.send(&ClientMessage::SetDamageVisualization { config }) + } + + pub fn set_animations_config(&self, config: crate::_private::AnimationsConfigIpc) { + self.send(&ClientMessage::SetAnimationsConfig { config }) + } + pub fn switch_to_vt(&self, vtnr: u32) { self.send(&ClientMessage::SwitchTo { vtnr }) } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 5130a98e..be54e511 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -1,8 +1,8 @@ use { crate::{ _private::{ - BlurConfigIpc, ClientCriterionIpc, LayerRuleIpc, PollableId, WindowCriterionIpc, - WireMode, + BlurConfigIpc, ClientCriterionIpc, DamageVisualizationIpc, LayerRuleIpc, PollableId, + WindowCriterionIpc, WireMode, }, Axis, Direction, PciId, Workspace, client::{Client, ClientCapabilities, ClientMatcher}, @@ -925,6 +925,12 @@ pub enum ClientMessage<'a> { SetBlurConfig { config: BlurConfigIpc, }, + SetDamageVisualization { + config: DamageVisualizationIpc, + }, + SetAnimationsConfig { + config: crate::_private::AnimationsConfigIpc, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index 4d83f1e9..220f0570 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -407,6 +407,34 @@ pub fn _set_blur_config(config: crate::_private::BlurConfigIpc) { get!().set_blur_config(config) } +#[doc(hidden)] +pub fn _set_damage_visualization(config: crate::_private::DamageVisualizationIpc) { + get!().set_damage_visualization(config) +} + +#[doc(hidden)] +pub fn _set_animations_config(config: crate::_private::AnimationsConfigIpc) { + get!().set_animations_config(config) +} + +/// Configures the damage region visualizer. +/// +/// When enabled, every damaged screen region is overlaid with `color` and fades +/// out over `decay` (producing a "blink" effect as new damage accumulates). +/// Useful for debugging damage-tracked rendering paths. +pub fn set_damage_visualization( + enabled: bool, + color: crate::theme::Color, + decay: std::time::Duration, +) { + let decay_millis = decay.as_millis().min(u64::MAX as u128) as u64; + _set_damage_visualization(crate::_private::DamageVisualizationIpc { + enabled, + color, + decay_millis, + }); +} + /// Returns the current corner radius for window borders. pub fn get_corner_radius() -> f32 { get!(0.0).get_corner_radius() diff --git a/src/animation.rs b/src/animation.rs new file mode 100644 index 00000000..ec08b1db --- /dev/null +++ b/src/animation.rs @@ -0,0 +1,193 @@ +use { + crate::{ + allocator::{BO_USE_RENDERING, BufferUsage}, + format::ARGB8888, + gfx_api::{AcquireSync, GfxTexture, ReleaseSync, needs_render_usage}, + rect::Rect, + renderer::Renderer, + state::State, + theme::Color, + tree::{OutputNode, ToplevelNode, Transform}, + video::Modifier, + }, + std::{cell::Cell, rc::Weak}, + std::rc::Rc, +}; + +/// A captured snapshot of a toplevel's last rendered state, used to drive the +/// close animation after the toplevel itself has been torn down. Owns its own +/// GPU texture so the source client buffers can be released immediately. +pub struct Snapshot { + pub texture: Rc, + /// The output the toplevel was on, used to schedule per-frame damage. + pub output: Weak, + /// Logical absolute position the toplevel occupied, used to draw the + /// snapshot into the correct screen region during the close animation. + pub rect: Rect, + /// Slide-out direction in logical pixels. The snapshot moves from (0, 0) + /// at start to (slide_dx, slide_dy) at end. + pub slide_dx: f32, + pub slide_dy: f32, + pub start_nsec: Cell, +} + +impl Snapshot { + /// Returns the eased close-animation progress in [0, 1], or None if the + /// animation has finished. + pub fn close_progress(&self, state: &State) -> Option { + let cfg = state.animations_config.get(); + if !cfg.enabled || cfg.close_duration_ms == 0 { + return None; + } + let now = state.now_nsec(); + let elapsed = now.saturating_sub(self.start_nsec.get()); + let dur = (cfg.close_duration_ms as u64).saturating_mul(1_000_000); + if elapsed >= dur { + return None; + } + let t = (elapsed as f32) / (dur as f32); + let eased = match cfg.close_curve { + jay_config::_private::AnimationCurveIpc::Linear => t, + jay_config::_private::AnimationCurveIpc::EaseOut => { + let inv = 1.0 - t; + 1.0 - inv * inv * inv + } + jay_config::_private::AnimationCurveIpc::EaseInOut => { + if t < 0.5 { + 4.0 * t * t * t + } else { + let f = -2.0 * t + 2.0; + 1.0 - f * f * f / 2.0 + } + } + jay_config::_private::AnimationCurveIpc::Bezier { x1, y1, x2, y2 } => { + cubic_bezier_y_at_x(t, x1, y1, x2, y2) + } + }; + Some(eased.clamp(0.0, 1.0)) + } +} + +fn cubic_bezier_y_at_x(x: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 { + fn bx(t: f32, x1: f32, x2: f32) -> f32 { + let it = 1.0 - t; + 3.0 * it * it * t * x1 + 3.0 * it * t * t * x2 + t * t * t + } + fn dbx(t: f32, x1: f32, x2: f32) -> f32 { + let it = 1.0 - t; + 3.0 * it * it * x1 + 6.0 * it * t * (x2 - x1) + 3.0 * t * t * (1.0 - x2) + } + let mut t = x; + for _ in 0..8 { + let err = bx(t, x1, x2) - x; + if err.abs() < 1e-4 { + break; + } + let d = dbx(t, x1, x2); + if d.abs() < 1e-6 { + break; + } + t = (t - err / d).clamp(0.0, 1.0); + } + let it = 1.0 - t; + 3.0 * it * it * t * y1 + 3.0 * it * t * t * y2 + t * t * t +} + +/// Renders the toplevel into a private GPU texture and returns the texture. +/// Used at unmap time to capture the last-rendered state, so a close animation +/// can run after the toplevel itself has been destroyed. Returns None if the +/// render context is unavailable, the toplevel has no workspace, or +/// allocation/rendering fails. +/// +/// Any open animation in flight on the toplevel is cleared before rendering so +/// the snapshot is at full opacity / final position. +pub fn capture_snapshot(state: &State, tl: &Rc) -> Option { + let ctx = state.render_ctx.get()?; + let formats = ctx.formats(); + let format_info = formats.get(&ARGB8888.drm)?; + let modifiers: Vec = format_info + .write_modifiers + .iter() + .filter(|(m, _)| format_info.read_modifiers.contains(*m)) + .map(|(m, _)| *m) + .collect(); + if modifiers.is_empty() { + return None; + } + let data = tl.tl_data(); + data.anim_open_start_nsec.set(None); + let workspace = data.workspace.get()?; + let output = workspace.output.get(); + let scale = output.global.persistent.scale.get(); + let scalef = scale.to_f64(); + let tl_rect = tl.node_absolute_position(); + let pw = (tl_rect.width() as f64 * scalef).round() as i32; + let ph = (tl_rect.height() as f64 * scalef).round() as i32; + if pw <= 0 || ph <= 0 { + return None; + } + let allocator = ctx.allocator(); + let mut usage = BO_USE_RENDERING; + if !needs_render_usage(format_info.write_modifiers.values()) { + usage = BufferUsage::none(); + } + let bo = allocator + .create_bo(&state.dma_buf_ids, pw, ph, ARGB8888, &modifiers, usage) + .ok()?; + let img = ctx.clone().dmabuf_img(bo.dmabuf()).ok()?; + let fb = img.clone().to_framebuffer().ok()?; + let mut ops = vec![]; + { + let mut renderer = Renderer { + base: fb.renderer_base(&mut ops, scale, Transform::None), + state, + logical_extents: tl_rect.at_point(0, 0), + pixel_extents: Rect::new_saturating(0, 0, pw, ph), + stretch: None, + corner_radius: None, + current_anim_node: None, + }; + tl.clone().node_render(&mut renderer, 0, 0, None); + } + let cd = state.color_manager.srgb_gamma22(); + fb.render( + AcquireSync::Unnecessary, + ReleaseSync::Implicit, + cd, + &ops, + Some(&Color::TRANSPARENT), + &cd.linear, + None, + cd, + ) + .ok()?; + let texture = img.to_texture().ok()?; + + // Slide-out direction: same closest-edge logic as the open animation, but + // the snapshot moves AWAY from its rect during close. Computed once here so + // we don't need the toplevel's tile lookup anymore once it's torn down. + let output_rect = output.global.pos.get(); + let dl = (tl_rect.x1() - output_rect.x1()).max(0) as f32; + let dr = (output_rect.x2() - tl_rect.x2()).max(0) as f32; + let dt = (tl_rect.y1() - output_rect.y1()).max(0) as f32; + let db = (output_rect.y2() - tl_rect.y2()).max(0) as f32; + let mind = dl.min(dr).min(dt).min(db); + let (slide_dx, slide_dy) = if mind == dl { + (-(tl_rect.width() as f32), 0.0) + } else if mind == dr { + (tl_rect.width() as f32, 0.0) + } else if mind == dt { + (0.0, -(tl_rect.height() as f32)) + } else { + (0.0, tl_rect.height() as f32) + }; + + Some(Snapshot { + texture, + output: Rc::downgrade(&output), + rect: tl_rect, + slide_dx, + slide_dy, + start_nsec: Cell::new(state.now_nsec()), + }) +} diff --git a/src/compositor.rs b/src/compositor.rs index ebe6e599..c8fbb5e9 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -399,6 +399,10 @@ fn start_compositor2( hyprland_global_shortcuts: Default::default(), layer_rules: Default::default(), blur_config: Default::default(), + blur_cache_epoch: Default::default(), + animations_config: Default::default(), + active_animations: Default::default(), + close_snapshots: Default::default(), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); diff --git a/src/config/handler.rs b/src/config/handler.rs index 18dbef71..07734fe3 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -3537,6 +3537,20 @@ impl ConfigProxyHandler { ClientMessage::SetBlurConfig { config } => { self.state.blur_config.set(config); } + ClientMessage::SetAnimationsConfig { config } => { + self.state.animations_config.set(config); + } + ClientMessage::SetDamageVisualization { config } => { + let [r, g, b, a] = config.color.to_u8_straight(); + let color = crate::theme::Color::from_srgba_straight(r, g, b, a); + self.state.damage_visualizer.set_color(color); + self.state + .damage_visualizer + .set_decay(std::time::Duration::from_millis(config.decay_millis)); + self.state + .damage_visualizer + .set_enabled(&self.state, config.enabled); + } } Ok(()) } diff --git a/src/gfx_api.rs b/src/gfx_api.rs index bbe9d222..61183b61 100644 --- a/src/gfx_api.rs +++ b/src/gfx_api.rs @@ -107,12 +107,36 @@ pub enum GfxApiOpt { BlurBackdrop(BlurBackdrop), } -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct BlurBackdrop { pub rect: FramebufferRect, pub passes: u8, pub offset: f32, pub mask: Option, + pub cache: Option>>>, + pub cache_epoch: u64, + pub cache_pixel_rect: [i32; 4], +} + +impl std::fmt::Debug for BlurBackdrop { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BlurBackdrop") + .field("rect", &self.rect) + .field("passes", &self.passes) + .field("offset", &self.offset) + .field("mask", &self.mask) + .field("cache_epoch", &self.cache_epoch) + .field("cache_pixel_rect", &self.cache_pixel_rect) + .finish() + } +} + +pub struct BlurCacheEntry { + pub pixel_rect: [i32; 4], + pub passes: u8, + pub offset: f32, + pub epoch: u64, + pub image: Rc, } #[derive(Clone)] @@ -120,6 +144,9 @@ pub struct BlurMask { pub texture: Rc, pub source: SampleRect, pub threshold: f32, + pub buffer_resv: Option>, + pub acquire_sync: AcquireSync, + pub release_sync: ReleaseSync, } impl std::fmt::Debug for BlurMask { @@ -785,6 +812,7 @@ impl dyn GfxFramebuffer { }, stretch: None, corner_radius: None, + current_anim_node: None, }; cursor.render_hardware_cursor(&mut renderer); self.render( @@ -1119,6 +1147,7 @@ pub fn create_render_pass( }, stretch: None, corner_radius: None, + current_anim_node: None, }; node.node_render(&mut renderer, 0, 0, None); if let Some(rect) = cursor_rect { @@ -1193,6 +1222,9 @@ pub fn renderer_base<'a>( fb_width: width as _, fb_height: height as _, discard_alpha: None, + alpha_mul: 1.0, + translate_x: 0.0, + translate_y: 0.0, } } diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index 877151c4..79a3f19b 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -1582,7 +1582,9 @@ impl WlSeatGlobal { { con.disconnect(TextDisconnectReason::FocusLost); } - if let Some(tis) = self.text_inputs.borrow().get(&surface.client.id) { + if !surface.destroyed.get() + && let Some(tis) = self.text_inputs.borrow().get(&surface.client.id) + { for ti in tis.lock().values() { ti.send_leave(surface); ti.send_done(); diff --git a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs index 7f7d053b..e31f90b1 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs @@ -26,7 +26,7 @@ use { Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, NodeLocation, NodeVisitor, OutputNode, StackedNode, }, - utils::{clonecell::CloneCell, smallmap::SmallMap}, + utils::{clonecell::CloneCell, numcell::NumCell, smallmap::SmallMap}, wire::{XdgPopupId, xdg_popup::*}, }, std::{ @@ -72,6 +72,7 @@ pub struct LayerPopupBlur { } pub struct XdgPopup { + pub blur_pre_rendered: Cell, pub id: XdgPopupId, node_id: PopupId, pub xdg: Rc, @@ -79,6 +80,8 @@ pub struct XdgPopup { relative_position: Cell, pos: RefCell, pub tracker: Tracker, + pub blur_cache: Rc>>, + pub blur_cache_epoch: NumCell, seat_state: NodeSeatState, set_visible_prepared: Cell, jay_popup_ext: CloneCell>>, @@ -104,8 +107,11 @@ impl XdgPopup { Ok(Self { id, node_id: xdg.surface.client.state.node_ids.next(), + blur_pre_rendered: Cell::new(false), xdg: xdg.clone(), parent: Default::default(), + blur_cache: Default::default(), + blur_cache_epoch: Default::default(), relative_position: Cell::new(Default::default()), pos: RefCell::new(pos), tracker: Default::default(), @@ -314,6 +320,9 @@ impl XdgPopupRequestHandler for XdgPopup { } impl XdgPopup { + pub fn layer_blur_settings(&self) -> Option { + self.parent.get()?.layer_blur_settings() + } pub fn set_visible(&self, visible: bool) { let surface = &self.xdg.surface; let extents = surface.extents.get(); @@ -418,33 +427,56 @@ impl Node for XdgPopup { } fn node_render(&self, renderer: &mut Renderer, x: i32, y: i32, bounds: Option<&Rect>) { - let settings = self.parent.get().and_then(|p| p.layer_blur_settings()); + let settings = self.layer_blur_settings(); if let Some(s) = settings { - if s.blur { + if s.blur && !self.blur_pre_rendered.get() { + // Only push blur if it wasn't already pushed in the pre-pass let extents = self.xdg.surface.extents.get(); let geo = self.xdg.geometry(); let (gx, gy) = geo.translate(x, y); let rect = extents.move_(gx, gy); - let scaled = renderer.base.scale_rect(rect); - let cfg = renderer.state.blur_config.get(); - let mask = s.ignore_alpha.and_then(|threshold| { - let buffer = self.xdg.surface.buffer.get()?; - let texture = buffer.buffer.buf.get_texture(&self.xdg.surface)?; - let source = *self.xdg.surface.buffer_points_norm.borrow(); - Some(crate::gfx_api::BlurMask { - texture, - source, - threshold, - }) - }); - renderer - .base - .push_blur_backdrop(scaled, cfg.passes, cfg.size, mask); + let popup_blur_rect = if let Some(parent) = self.parent.get() { + let parent_rect = parent.position(); + if parent_rect.contains_rect(&rect) { + None + } else { + Some(rect) + } + } else { + Some(rect) + }; + if let Some(blur_rect) = popup_blur_rect { + let scaled = renderer.base.scale_rect(blur_rect); + let cfg = renderer.state.blur_config.get(); + let mask = s.ignore_alpha.and_then(|threshold| { + let buffer = self.xdg.surface.buffer.get()?; + let texture = buffer.buffer.buf.get_texture(&self.xdg.surface)?; + let source = *self.xdg.surface.buffer_points_norm.borrow(); + let release_sync = buffer.release_sync; + Some(crate::gfx_api::BlurMask { + texture, + source, + threshold, + buffer_resv: Some(buffer), + acquire_sync: crate::gfx_api::AcquireSync::Unnecessary, + release_sync, + }) + }); + renderer.base.push_blur_backdrop( + scaled, + cfg.passes, + cfg.size, + mask, + Some(self.blur_cache.clone()), + self.blur_cache_epoch.get(), + ); + } } - renderer.base.discard_alpha = s.ignore_alpha; + // Always clear the flag after node_render regardless of path + self.blur_pre_rendered.set(false); renderer.render_xdg_surface(&self.xdg, x, y, bounds); - renderer.base.discard_alpha = None; } else { + self.blur_pre_rendered.set(false); renderer.render_xdg_surface(&self.xdg, x, y, bounds); } } diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index 768a8367..28728e28 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -555,6 +555,7 @@ impl XdgToplevel { self.state.tree_changed(); self.toplevel_data.mapped_source.trigger(); self.toplevel_data.broadcast(self.clone()); + self.toplevel_data.start_open_animation(); } self.toplevel_data .set_content_type(self.xdg.surface.content_type.get()); diff --git a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs index f3b1e45f..8829fc9d 100644 --- a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs +++ b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs @@ -53,6 +53,7 @@ pub struct ZwlrLayerSurfaceV1 { pub client: Rc, pub surface: Rc, pub output: Rc, + pub blur_cache_epoch: NumCell, pub namespace: String, pub tracker: Tracker, output_extents: Cell, @@ -62,6 +63,7 @@ pub struct ZwlrLayerSurfaceV1 { pub blur: Cell, pub blur_popups: Cell, pub ignore_alpha: Cell>, + pub blur_cache: Rc>>, requested_serial: NumCell, size: Cell<(i32, i32)>, anchor: Cell, @@ -158,6 +160,7 @@ impl ZwlrLayerSurfaceV1 { ) -> Self { Self { id, + blur_cache_epoch: Default::default(), node_id: shell.client.state.node_ids.next(), shell: shell.clone(), client: shell.client.clone(), @@ -172,6 +175,7 @@ impl ZwlrLayerSurfaceV1 { blur: Cell::new(false), blur_popups: Cell::new(false), ignore_alpha: Cell::new(None), + blur_cache: Default::default(), requested_serial: Default::default(), size: Cell::new((0, 0)), anchor: Cell::new(0), diff --git a/src/main.rs b/src/main.rs index 5a566f9b..161d3d99 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,6 +48,7 @@ mod leaks; mod tracy; mod acceptor; mod allocator; +mod animation; mod async_engine; mod backend; mod backends; diff --git a/src/renderer.rs b/src/renderer.rs index ddb5ab7c..c3a97c2f 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -5,7 +5,7 @@ use { ifs::wl_surface::{ SurfaceBuffer, WlSurface, x_surface::xwindow::Xwindow, - xdg_surface::{XdgSurface, xdg_toplevel::XdgToplevel}, + xdg_surface::{XdgSurface, xdg_popup::XdgPopup, xdg_toplevel::XdgToplevel}, zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, }, rect::Rect, @@ -14,8 +14,9 @@ use { state::State, theme::{Color, CornerRadius}, tree::{ - ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData, - ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar, + ContainerNode, DisplayNode, FloatNode, NodeId, OutputNode, PlaceholderNode, + StackedNode, ToplevelData, ToplevelNode, ToplevelNodeBase, WorkspaceNode, + tab_bar::TabBar, }, }, std::{ops::Deref, rc::Rc, slice}, @@ -30,9 +31,60 @@ pub struct Renderer<'a> { pub pixel_extents: Rect, pub stretch: Option<(i32, i32)>, pub corner_radius: Option, + /// The toplevel whose open-animation transform is currently applied to the + /// renderer base. Used to prevent double-applying when a parent (container, + /// float) has already entered the animation scope before drawing its own + /// per-child decorations. + pub current_anim_node: Option, +} + +#[must_use] +pub struct OpenAnimSaved { + alpha_mul: f32, + translate_x: f32, + translate_y: f32, + prev_node: Option, } impl Renderer<'_> { + pub fn render_layer_popup_blur_only(&mut self, popup: &XdgPopup, x: i32, y: i32) { + let Some(settings) = popup.layer_blur_settings() else { + return; + }; + if !settings.blur { + return; + } + let extents = popup.xdg.surface.extents.get(); + let geo = popup.xdg.geometry(); + let (gx, gy) = geo.translate(x, y); + let rect = extents.move_(gx, gy); + let scaled = self.base.scale_rect(rect); + let cfg = self.state.blur_config.get(); + let mask = settings.ignore_alpha.and_then(|threshold| { + let buffer = popup.xdg.surface.buffer.get()?; + let texture = buffer.buffer.buf.get_texture(&popup.xdg.surface)?; + let source = *popup.xdg.surface.buffer_points_norm.borrow(); + let release_sync = buffer.release_sync; + Some(crate::gfx_api::BlurMask { + texture, + source, + threshold, + buffer_resv: Some(buffer), + acquire_sync: AcquireSync::Unnecessary, + release_sync, + }) + }); + popup.blur_pre_rendered.set(true); + self.base.push_blur_backdrop( + scaled, + cfg.passes, + cfg.size, + mask, + Some(popup.blur_cache.clone()), + popup.blur_cache_epoch.get(), + ); + self.base.sync(); + } pub fn scale(&self) -> Scale { self.base.scale } @@ -215,14 +267,28 @@ impl Renderer<'_> { }; } render_stacked!(self.state.root.stacked); - // Flush RoundedFillRect ops from container/float borders so they don't - // sort after (and render on top of) layer-shell CopyTexture ops. self.base.sync(); if fullscreen.is_none() { + // Pre-pass: push blur backdrops for layer-shell popups before + // the bar renders, so they sample the raw background rather than + // the already-composited bar content. + for stacked in self.state.root.stacked_above_layers.iter() { + if stacked.node_visible() { + let pos = stacked.node_absolute_position(); + if pos.intersects(&opos) { + let (sx, sy) = opos.translate(pos.x1(), pos.y1()); + let stacked_rc: Rc = stacked.deref().clone(); + if let Some(popup) = stacked_rc.node_into_popup() { + self.render_layer_popup_blur_only(&popup, sx, sy); + } + } + } + } render_layer!(output.layers[2]); } render_layer!(output.layers[3]); render_stacked!(self.state.root.stacked_above_layers); + self.render_close_snapshots(output, x, y); if let Some(ws) = output.workspace.get() && ws.render_highlight.get() > 0 { @@ -407,6 +473,7 @@ impl Renderer<'_> { self.render_tab_bar(tb, x, y, container.width.get()); } } + let saved_anim = self.enter_open_anim(&*child.node); let mb = container.mono_body.get(); if self.state.theme.sizes.gap.get() != 0 { let srgb_srgb = self.state.color_manager.srgb_gamma22(); @@ -487,6 +554,7 @@ impl Renderer<'_> { .node_render(self, x + content.x1(), y + content.y1(), Some(&body)); self.stretch = None; self.corner_radius = None; + self.exit_open_anim(saved_anim); } else { let gap = self.state.theme.sizes.gap.get(); let (srgb_srgb, bw, border_color, focused_border_color) = if gap != 0 { @@ -504,6 +572,7 @@ impl Renderer<'_> { if body.x1() >= container.width.get() || body.y1() >= container.height.get() { break; } + let saved_anim = self.enter_open_anim(&*child.node); if let Some(srgb_srgb) = srgb_srgb { let srgb = &srgb_srgb.linear; let c = if child.border_color_is_focused.get() { @@ -574,6 +643,7 @@ impl Renderer<'_> { .node_render(self, x + content.x1(), y + content.y1(), Some(&body)); self.stretch = None; self.corner_radius = None; + self.exit_open_anim(saved_anim); } } @@ -581,13 +651,135 @@ impl Renderer<'_> { } pub fn render_xwindow(&mut self, tl: &Xwindow, x: i32, y: i32, bounds: Option<&Rect>) { + let saved = self.enter_open_anim(tl); + let bounds = if tl.tl_data().anim_open_alpha().is_some() { + None + } else { + bounds + }; self.render_surface(&tl.x.surface, x, y, bounds); self.render_tl_aux(tl.tl_data(), bounds, true); + self.exit_open_anim(saved); } pub fn render_xdg_toplevel(&mut self, tl: &XdgToplevel, x: i32, y: i32, bounds: Option<&Rect>) { + let saved = self.enter_open_anim(tl); + let bounds = if tl.tl_data().anim_open_alpha().is_some() { + None + } else { + bounds + }; self.render_xdg_surface(&tl.xdg, x, y, bounds); self.render_tl_aux(tl.tl_data(), bounds, true); + self.exit_open_anim(saved); + } + + /// Enters open-animation scope for `tl`: applies its eased alpha + slide + /// translate to the renderer base. If a parent has already entered scope + /// for the same toplevel (so its borders/decorations slide too), this is a + /// no-op and returns `None`. Pair every `Some` return with `exit_open_anim`. + pub fn enter_open_anim(&mut self, tl: &dyn ToplevelNode) -> Option { + let data = tl.tl_data(); + let eased = data.anim_open_alpha()?; + if self.current_anim_node == Some(data.node_id) { + return None; + } + let saved = OpenAnimSaved { + alpha_mul: self.base.alpha_mul, + translate_x: self.base.translate_x, + translate_y: self.base.translate_y, + prev_node: self.current_anim_node, + }; + self.current_anim_node = Some(data.node_id); + self.base.alpha_mul *= eased; + if let Some(ws) = data.workspace.get() { + let tl_rect = tl.node_absolute_position(); + let output_rect = ws.output.get().global.pos.get(); + let dl = (tl_rect.x1() - output_rect.x1()).max(0) as f32; + let dr = (output_rect.x2() - tl_rect.x2()).max(0) as f32; + let dt = (tl_rect.y1() - output_rect.y1()).max(0) as f32; + let db = (output_rect.y2() - tl_rect.y2()).max(0) as f32; + let mind = dl.min(dr).min(dt).min(db); + let (sx, sy) = if mind == dl { + (-(tl_rect.width() as f32), 0.0) + } else if mind == dr { + (tl_rect.width() as f32, 0.0) + } else if mind == dt { + (0.0, -(tl_rect.height() as f32)) + } else { + (0.0, tl_rect.height() as f32) + }; + let factor = (1.0 - eased) * self.base.scalef as f32; + self.base.translate_x += sx * factor; + self.base.translate_y += sy * factor; + } + Some(saved) + } + + pub fn exit_open_anim(&mut self, saved: Option) { + if let Some(s) = saved { + self.base.alpha_mul = s.alpha_mul; + self.base.translate_x = s.translate_x; + self.base.translate_y = s.translate_y; + self.current_anim_node = s.prev_node; + } + } + + /// Renders any active close-animation snapshots that belong to this output. + /// Each snapshot fades out and slides toward its closest output edge — + /// mirroring the open animation in reverse. Finished snapshots stay in the + /// list until `tick_animations` cleans them up. + fn render_close_snapshots(&mut self, output: &OutputNode, x: i32, y: i32) { + let snaps = self.state.close_snapshots.borrow(); + if snaps.is_empty() { + return; + } + let output_pos = output.global.pos.get(); + for snap in snaps.iter() { + let Some(snap_output) = snap.output.upgrade() else { + continue; + }; + if !std::ptr::eq(&*snap_output, output) { + continue; + } + let Some(progress) = snap.close_progress(self.state) else { + continue; + }; + let alpha = (1.0 - progress).clamp(0.0, 1.0); + let prev_alpha = self.base.alpha_mul; + let prev_tx = self.base.translate_x; + let prev_ty = self.base.translate_y; + self.base.alpha_mul *= alpha; + self.base.translate_x += snap.slide_dx * progress * self.base.scalef as f32; + self.base.translate_y += snap.slide_dy * progress * self.base.scalef as f32; + let local_x = x + snap.rect.x1() - output_pos.x1(); + let local_y = y + snap.rect.y1() - output_pos.y1(); + let (sx, sy) = self.base.scale_point(local_x, local_y); + let scalef = self.base.scalef; + let tw = (snap.rect.width() as f64 * scalef).round() as i32; + let th = (snap.rect.height() as f64 * scalef).round() as i32; + let cd = self.state.color_manager.srgb_gamma22(); + self.base.render_texture( + &snap.texture, + None, + sx, + sy, + None, + Some((tw, th)), + self.base.scale, + None, + None, + AcquireSync::Unnecessary, + ReleaseSync::Implicit, + false, + cd, + RenderIntent::Perceptual, + AlphaMode::PremultipliedElectrical, + ); + self.base.alpha_mul = prev_alpha; + self.base.translate_x = prev_tx; + self.base.translate_y = prev_ty; + } } pub fn render_xdg_surface( @@ -804,6 +996,7 @@ impl Renderer<'_> { Some(c) => c, _ => return, }; + let saved_anim = self.enter_open_anim(&*child); let pos = floating.position.get(); let theme = &self.state.theme; let bw = theme.sizes.border_width.get(); @@ -848,11 +1041,13 @@ impl Renderer<'_> { } child.node_render(self, body.x1(), body.y1(), Some(&scissor_body)); self.corner_radius = None; + self.exit_open_anim(saved_anim); } pub fn render_layer_surface(&mut self, surface: &ZwlrLayerSurfaceV1, x: i32, y: i32) { let (dx, dy) = surface.surface.extents.get().position(); let blur = surface.blur.get(); + let ignore_alpha = surface.ignore_alpha.get(); if blur { let extents = surface.surface.extents.get(); @@ -863,18 +1058,22 @@ impl Renderer<'_> { let buffer = surface.surface.buffer.get()?; let texture = buffer.buffer.buf.get_texture(&surface.surface)?; let source = *surface.surface.buffer_points_norm.borrow(); + let release_sync = buffer.release_sync; Some(crate::gfx_api::BlurMask { texture, source, threshold, + buffer_resv: Some(buffer), + acquire_sync: AcquireSync::Unnecessary, + release_sync, }) }); + let cache_epoch = surface.blur_cache_epoch.get(); + let cache = Some(surface.blur_cache.clone()); self.base - .push_blur_backdrop(scaled, cfg.passes, cfg.size, mask); + .push_blur_backdrop(scaled, cfg.passes, cfg.size, mask, cache, cache_epoch); } - self.base.discard_alpha = ignore_alpha; self.render_surface(&surface.surface, x - dx, y - dy, None); - self.base.discard_alpha = None; } fn bounds_are_opaque( diff --git a/src/renderer/renderer_base.rs b/src/renderer/renderer_base.rs index f2c95265..467e25e8 100644 --- a/src/renderer/renderer_base.rs +++ b/src/renderer/renderer_base.rs @@ -26,6 +26,9 @@ pub struct RendererBase<'a> { pub fb_width: f32, pub fb_height: f32, pub discard_alpha: Option, + pub alpha_mul: f32, + pub translate_x: f32, + pub translate_y: f32, } impl RendererBase<'_> { @@ -33,6 +36,26 @@ impl RendererBase<'_> { self.scale } + fn apply_alpha_mul(&self, alpha: Option) -> Option { + if self.alpha_mul >= 1.0 { + alpha + } else { + Some(alpha.unwrap_or(1.0) * self.alpha_mul) + } + } + + fn fb_rect(&self, x1: f32, y1: f32, x2: f32, y2: f32) -> FramebufferRect { + FramebufferRect::new( + x1 + self.translate_x, + y1 + self.translate_y, + x2 + self.translate_x, + y2 + self.translate_y, + self.transform, + self.fb_width, + self.fb_height, + ) + } + pub fn scale_point(&self, mut x: i32, mut y: i32) -> (i32, i32) { if self.scaled { [x, y] = self.scale.pixel_size([x, y]); @@ -123,17 +146,14 @@ impl RendererBase<'_> { true => bx, }; self.ops.push(GfxApiOpt::FillRect(FillRect { - rect: FramebufferRect::new( + rect: self.fb_rect( bx.x1() as f32, bx.y1() as f32, bx.x2() as f32, bx.y2() as f32, - self.transform, - self.fb_width, - self.fb_height, ), color: *color, - alpha, + alpha: self.apply_alpha_mul(alpha), render_intent, cd: cd.clone(), })); @@ -166,17 +186,9 @@ impl RendererBase<'_> { for bx in boxes { let (x1, y1, x2, y2) = self.scale_rect_f(*bx); self.ops.push(GfxApiOpt::FillRect(FillRect { - rect: FramebufferRect::new( - x1 + dx, - y1 + dy, - x2 + dx, - y2 + dy, - self.transform, - self.fb_width, - self.fb_height, - ), + rect: self.fb_rect(x1 + dx, y1 + dy, x2 + dx, y2 + dy), color: *color, - alpha: None, + alpha: self.apply_alpha_mul(None), render_intent, cd: cd.clone(), })); @@ -227,21 +239,20 @@ impl RendererBase<'_> { return; } - let target = FramebufferRect::new( + let target = self.fb_rect( target_x[0] as f32, target_y[0] as f32, target_x[1] as f32, target_y[1] as f32, - self.transform, - self.fb_width, - self.fb_height, ); + let new_alpha = self.apply_alpha_mul(alpha); + let opaque = opaque && new_alpha == alpha; self.ops.push(GfxApiOpt::CopyTexture(CopyTexture { tex: texture.clone(), source: texcoord, target, - alpha, + alpha: new_alpha, buffer_resv, acquire_sync, release_sync, @@ -296,17 +307,14 @@ impl RendererBase<'_> { let fitted = corner_radius.fit_to(width, height); let cr: [f32; 4] = fitted.into(); self.ops.push(GfxApiOpt::RoundedFillRect(RoundedFillRect { - rect: FramebufferRect::new( + rect: self.fb_rect( rect.x1() as f32, rect.y1() as f32, rect.x2() as f32, rect.y2() as f32, - self.transform, - self.fb_width, - self.fb_height, ), color: *color, - alpha, + alpha: self.apply_alpha_mul(alpha), render_intent, cd: cd.clone(), size: [width, height], @@ -358,14 +366,11 @@ impl RendererBase<'_> { return; } - let target = FramebufferRect::new( + let target = self.fb_rect( target_x[0] as f32, target_y[0] as f32, target_x[1] as f32, target_y[1] as f32, - self.transform, - self.fb_width, - self.fb_height, ); let width = (target_x[1] - target_x[0]) as f32; @@ -379,7 +384,7 @@ impl RendererBase<'_> { tex: texture.clone(), source: texcoord, target, - alpha, + alpha: self.apply_alpha_mul(alpha), buffer_resv, acquire_sync, release_sync, @@ -404,6 +409,8 @@ impl RendererBase<'_> { passes: u8, offset: f32, mask: Option, + cache: Option>>>, + cache_epoch: u64, ) { let target = FramebufferRect::new( rect.x1() as f32, @@ -414,11 +421,15 @@ impl RendererBase<'_> { self.fb_width, self.fb_height, ); + let cache_pixel_rect = [rect.x1(), rect.y1(), rect.x2(), rect.y2()]; self.ops.push(GfxApiOpt::BlurBackdrop(BlurBackdrop { rect: target, passes, offset, mask, + cache, + cache_epoch, + cache_pixel_rect, })); } } diff --git a/src/state.rs b/src/state.rs index 9decfcd9..136597d1 100644 --- a/src/state.rs +++ b/src/state.rs @@ -306,6 +306,10 @@ pub struct State { pub hyprland_global_shortcuts: CopyHashMap<(String, String), Rc>, pub layer_rules: RefCell>, pub blur_config: Cell, + pub blur_cache_epoch: NumCell, + pub animations_config: Cell, + pub active_animations: RefCell>>, + pub close_snapshots: RefCell>>, } // impl Drop for State { @@ -1049,6 +1053,25 @@ impl State { if rect.is_empty() { return; } + if !cursor { + for output in self.root.outputs.lock().values() { + for layer in &output.layers { + for surface in layer.iter() { + if surface.blur.get() && surface.node_absolute_position().intersects(&rect) + { + surface.blur_cache_epoch.fetch_add(1); + } + if surface.blur.get() && surface.blur_popups.get() { + surface.for_each_popup(|popup| { + if popup.node_absolute_position().intersects(&rect) { + popup.blur_cache_epoch.fetch_add(1); + } + }); + } + } + } + } + } self.damage_visualizer.add(rect); for output in self.root.outputs.lock().values() { if output.global.pos.get().intersects(&rect) { @@ -1290,6 +1313,7 @@ impl State { }, stretch: None, corner_radius: None, + current_anim_node: None, }; let mut sample_rect = SampleRect::identity(); sample_rect.buffer_transform = transform; @@ -1464,6 +1488,55 @@ impl State { self.eng.now().msec() } + /// Walks the active-animations list, damages each toplevel's slide region + /// (so the next frame re-renders it), and removes any whose animation is + /// done. Also ticks close-animation snapshots: damages their output and + /// drops finished ones. Intended to be called once per output present cycle. + pub fn tick_animations(&self) { + { + let mut animations = self.active_animations.borrow_mut(); + if !animations.is_empty() { + animations.retain(|weak| { + let Some(tl) = weak.upgrade() else { + return false; + }; + let data = tl.tl_data(); + if data.anim_open_alpha().is_none() { + return false; + } + // Damage the entire output the toplevel is on: the slide + // can render outside the toplevel's nominal rect, so the + // narrow rect alone would leave the slid-out portion + // unredrawn. + if let Some(ws) = data.workspace.get() { + self.damage(ws.output.get().global.pos.get()); + } else { + self.damage(tl.node_absolute_position()); + } + true + }); + } + } + let mut snapshots = self.close_snapshots.borrow_mut(); + if snapshots.is_empty() { + return; + } + snapshots.retain(|snap| { + if snap.close_progress(self).is_none() { + // Final damage so the snapshot's last-rendered position gets + // repainted (clearing any leftover pixels). + if let Some(output) = snap.output.upgrade() { + self.damage(output.global.pos.get()); + } + return false; + } + if let Some(output) = snap.output.upgrade() { + self.damage(output.global.pos.get()); + } + true + }); + } + pub fn output_extents_changed(&self) { self.root.update_extents(); for seat in self.globals.seats.lock().values() { diff --git a/src/tree/output.rs b/src/tree/output.rs index 611eb822..b18c6624 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -236,6 +236,7 @@ impl OutputNode { for listener in self.presentation_event.iter() { listener.presented(self, tv_sec, tv_nsec, refresh, seq, flags, vrr); } + self.state.tick_animations(); if locked && let Some(lock) = self.state.lock.lock.get() { lock.check_locked() } diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 02bba848..752d9d77 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -428,6 +428,7 @@ pub struct ToplevelData { pub property_changed_source: OnceCell>, pub mapped_source: Rc, pub unmapped_source: Rc, + pub anim_open_start_nsec: Cell>, } impl ToplevelData { @@ -485,6 +486,7 @@ impl ToplevelData { property_changed_source: Default::default(), mapped_source: state.lazy_event_sources.create_source(), unmapped_source: state.lazy_event_sources.create_source(), + anim_open_start_nsec: Cell::new(None), } } @@ -531,6 +533,63 @@ impl ToplevelData { } } + /// Returns the eased alpha multiplier for the open animation, or None if no + /// animation is active. When the animation has finished, clears the start + /// time and returns None. + pub fn anim_open_alpha(&self) -> Option { + let start = self.anim_open_start_nsec.get()?; + let cfg = self.state.animations_config.get(); + if !cfg.enabled || cfg.open_duration_ms == 0 { + self.anim_open_start_nsec.set(None); + return None; + } + let now = self.state.now_nsec(); + let elapsed_ns = now.saturating_sub(start); + let dur_ns = (cfg.open_duration_ms as u64).saturating_mul(1_000_000); + if elapsed_ns >= dur_ns { + self.anim_open_start_nsec.set(None); + return None; + } + let t = (elapsed_ns as f32) / (dur_ns as f32); + let eased = match cfg.open_curve { + jay_config::_private::AnimationCurveIpc::Linear => t, + jay_config::_private::AnimationCurveIpc::EaseOut => { + let inv = 1.0 - t; + 1.0 - inv * inv * inv + } + jay_config::_private::AnimationCurveIpc::EaseInOut => { + if t < 0.5 { + 4.0 * t * t * t + } else { + let f = -2.0 * t + 2.0; + 1.0 - f * f * f / 2.0 + } + } + jay_config::_private::AnimationCurveIpc::Bezier { x1, y1, x2, y2 } => { + cubic_bezier_y_at_x(t, x1, y1, x2, y2) + } + }; + Some(eased.clamp(0.0, 1.0)) + } + + /// Starts the open animation if animations are enabled. Inserts the + /// toplevel into the state's active-animations list so the present loop + /// can drive redraws. + pub fn start_open_animation(&self) { + let cfg = self.state.animations_config.get(); + if !cfg.enabled || cfg.open_duration_ms == 0 { + return; + } + if self.anim_open_start_nsec.get().is_some() { + return; + } + self.anim_open_start_nsec.set(Some(self.state.now_nsec())); + self.state + .active_animations + .borrow_mut() + .push(self.slf.clone()); + } + pub fn property_changed(&self, change: TlMatcherChange) { self.trigger_property_source(); let mgr = &self.state.tl_matcher_manager; @@ -1108,3 +1167,31 @@ pub fn toplevel_set_workspace(state: &Rc, tl: Rc, ws: & tl.tl_set_fullscreen(true, Some(ws.clone())); } } + +/// Evaluates a cubic Bezier easing curve `cubic-bezier(x1, y1, x2, y2)` at the +/// given input time `x`. P0=(0,0) and P3=(1,1) are fixed. Uses Newton-Raphson +/// to invert the x(t) parametric form, then evaluates y(t). +fn cubic_bezier_y_at_x(x: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 { + fn bx(t: f32, x1: f32, x2: f32) -> f32 { + let it = 1.0 - t; + 3.0 * it * it * t * x1 + 3.0 * it * t * t * x2 + t * t * t + } + fn dbx(t: f32, x1: f32, x2: f32) -> f32 { + let it = 1.0 - t; + 3.0 * it * it * x1 + 6.0 * it * t * (x2 - x1) + 3.0 * t * t * (1.0 - x2) + } + let mut t = x; + for _ in 0..8 { + let err = bx(t, x1, x2) - x; + if err.abs() < 1e-4 { + break; + } + let d = dbx(t, x1, x2); + if d.abs() < 1e-6 { + break; + } + t = (t - err / d).clamp(0.0, 1.0); + } + let it = 1.0 - t; + 3.0 * it * it * t * y1 + 3.0 * it * t * t * y2 + t * t * t +} diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 66326d57..1e408803 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -393,6 +393,30 @@ pub struct BlurConfig { pub size: Option, } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum AnimationCurve { + Linear, + EaseOut, + EaseInOut, + Bezier { x1: f32, y1: f32, x2: f32, y2: f32 }, +} + +#[derive(Debug, Clone, Copy)] +pub struct AnimationsConfig { + pub enabled: Option, + pub open_duration_ms: Option, + pub open_curve: Option, + pub close_duration_ms: Option, + pub close_curve: Option, +} + +#[derive(Debug, Clone, Copy)] +pub struct DamageVisualization { + pub enabled: Option, + pub color: Option, + pub decay_ms: Option, +} + #[derive(Debug, Clone)] pub enum DrmDeviceMatch { Any(Vec), @@ -607,6 +631,8 @@ pub struct Config { pub window_rules: Vec, pub layer_rules: Vec, pub blur: Option, + pub damage_visualization: Option, + pub animations: Option, pub pointer_revert_key: Option, pub use_hardware_cursor: Option, pub show_bar: Option, diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index 766522b3..8e6a2404 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -8,6 +8,7 @@ use { pub mod action; mod actions; +mod animations; mod blur; mod capabilities; mod clean_logs_older_than; @@ -19,6 +20,7 @@ pub mod config; mod connector; mod connector_match; mod content_type; +mod damage_visualization; mod drm_device; mod drm_device_match; mod env; diff --git a/toml-config/src/config/parsers/animations.rs b/toml-config/src/config/parsers/animations.rs new file mode 100644 index 00000000..747b98f5 --- /dev/null +++ b/toml-config/src/config/parsers/animations.rs @@ -0,0 +1,73 @@ +use { + crate::{ + config::{ + AnimationCurve, AnimationsConfig, + context::Context, + extractor::{Extractor, ExtractorError, bol, int, opt, recover, str}, + parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + }, + toml::{ + toml_span::{DespanExt, Span, Spanned, SpannedExt}, + toml_value::Value, + }, + }, + indexmap::IndexMap, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum AnimationsConfigParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error(transparent)] + Extract(#[from] ExtractorError), + #[error("unknown animation curve `{0}`; expected one of: linear, ease-out, ease-in-out")] + UnknownCurve(String), +} + +pub struct AnimationsConfigParser<'a>(pub &'a Context<'a>); + +impl Parser for AnimationsConfigParser<'_> { + type Value = AnimationsConfig; + type Error = AnimationsConfigParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + let mut ext = Extractor::new(self.0, span, table); + let (enabled_val, open_duration_val, open_curve_val, close_duration_val, close_curve_val) = + ext.extract(( + recover(opt(bol("enabled"))), + recover(opt(int("open-duration-ms"))), + opt(str("open-curve")), + recover(opt(int("close-duration-ms"))), + opt(str("close-curve")), + ))?; + let enabled = enabled_val.despan(); + let open_duration_ms = open_duration_val.despan().and_then(|v| u32::try_from(v).ok()); + let close_duration_ms = close_duration_val.despan().and_then(|v| u32::try_from(v).ok()); + let parse_curve = |val: Option>| match val { + Some(s) => match s.value { + "linear" => Ok(Some(AnimationCurve::Linear)), + "ease-out" => Ok(Some(AnimationCurve::EaseOut)), + "ease-in-out" => Ok(Some(AnimationCurve::EaseInOut)), + other => { + Err(AnimationsConfigParserError::UnknownCurve(other.to_string()).spanned(s.span)) + } + }, + None => Ok(None), + }; + let open_curve = parse_curve(open_curve_val)?; + let close_curve = parse_curve(close_curve_val)?; + Ok(AnimationsConfig { + enabled, + open_duration_ms, + open_curve, + close_duration_ms, + close_curve, + }) + } +} diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index ab169895..a3df8be3 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -8,8 +8,10 @@ use { parsers::{ action::ActionParser, actions::ActionsParser, + animations::AnimationsConfigParser, blur::BlurConfigParser, clean_logs_older_than::CleanLogsOlderThanParser, + damage_visualization::DamageVisualizationParser, client_rule::ClientRulesParser, color_management::ColorManagementParser, connector::ConnectorsParser, @@ -157,7 +159,7 @@ impl Parser for ConfigParser<'_> { mouse_follows_focus, layer_rules_val, ), - (blur_val,), + (blur_val, damage_visualization_val, animations_val), ) = ext.extract(( ( opt(val("keymap")), @@ -219,7 +221,11 @@ impl Parser for ConfigParser<'_> { recover(opt(bol("unstable-mouse-follows-focus"))), opt(val("layers")), ), - (opt(val("blur")),), + ( + opt(val("blur")), + opt(val("damage-visualization")), + opt(val("animations")), + ), ))?; let mut keymap = None; if let Some(value) = keymap_val { @@ -515,6 +521,26 @@ impl Parser for ConfigParser<'_> { Err(e) => log::warn!("Could not parse the blur config: {}", self.0.error(e)), } } + let mut damage_visualization = None; + if let Some(value) = damage_visualization_val { + match value.parse(&mut DamageVisualizationParser(self.0)) { + Ok(v) => damage_visualization = Some(v), + Err(e) => log::warn!( + "Could not parse the damage-visualization config: {}", + self.0.error(e) + ), + } + } + let mut animations = None; + if let Some(value) = animations_val { + match value.parse(&mut AnimationsConfigParser(self.0)) { + Ok(v) => animations = Some(v), + Err(e) => log::warn!( + "Could not parse the animations config: {}", + self.0.error(e) + ), + } + } let mut pointer_revert_key = None; if let Some(value) = pointer_revert_key_str { match Keysym::from_str(value.value) { @@ -616,6 +642,8 @@ impl Parser for ConfigParser<'_> { window_rules, layer_rules, blur, + damage_visualization, + animations, pointer_revert_key, use_hardware_cursor: use_hardware_cursor.despan(), show_bar: show_bar.despan(), diff --git a/toml-config/src/config/parsers/damage_visualization.rs b/toml-config/src/config/parsers/damage_visualization.rs new file mode 100644 index 00000000..65feeb80 --- /dev/null +++ b/toml-config/src/config/parsers/damage_visualization.rs @@ -0,0 +1,65 @@ +use { + crate::{ + config::{ + DamageVisualization, + context::Context, + extractor::{Extractor, ExtractorError, bol, int, opt, recover, str}, + parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + parsers::color::{ColorParser, ColorParserError}, + }, + toml::{ + toml_span::{DespanExt, Span, Spanned, SpannedExt}, + toml_value::Value, + }, + }, + indexmap::IndexMap, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum DamageVisualizationParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error(transparent)] + Extract(#[from] ExtractorError), + #[error(transparent)] + Color(#[from] ColorParserError), +} + +pub struct DamageVisualizationParser<'a>(pub &'a Context<'a>); + +impl Parser for DamageVisualizationParser<'_> { + type Value = DamageVisualization; + type Error = DamageVisualizationParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + let mut ext = Extractor::new(self.0, span, table); + let (enabled_val, color_val, decay_val) = ext.extract(( + recover(opt(bol("enabled"))), + opt(str("color")), + recover(opt(int("decay-ms"))), + ))?; + let enabled = enabled_val.despan(); + let color = match color_val { + Some(s) => match ColorParser.parse_string(s.span, s.value) { + Ok(c) => Some(c), + Err(e) => { + return Err(DamageVisualizationParserError::Color(e.value) + .spanned(s.span)); + } + }, + None => None, + }; + let decay_ms = decay_val.despan().and_then(|v| u64::try_from(v).ok()); + Ok(DamageVisualization { + enabled, + color, + decay_ms, + }) + } +} diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 85b92127..49b85f66 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -13,7 +13,8 @@ mod toml; use { crate::{ config::{ - Action, BlurConfig, ClientRule, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap, + Action, AnimationCurve, AnimationsConfig, BlurConfig, ClientRule, Config, + ConfigConnector, ConfigDrmDevice, ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, LayerKind, LayerRule, Output, OutputMatch, SimpleCommand, Status, Theme, WindowRule, parse_config, }, @@ -23,8 +24,12 @@ use { ahash::{AHashMap, AHashSet}, error_reporter::Report, jay_config::{ - _private::{BlurConfigIpc, LayerKindIpc, LayerMatchIpc, LayerRuleIpc}, - _set_blur_config, _set_layer_rules, Axis, + _private::{ + AnimationCurveIpc, AnimationsConfigIpc, BlurConfigIpc, DamageVisualizationIpc, + LayerKindIpc, LayerMatchIpc, LayerRuleIpc, + }, + _set_animations_config, _set_blur_config, _set_damage_visualization, _set_layer_rules, + Axis, client::Client, config, config_dir, exec::{Command, set_env, unset_env}, @@ -1471,6 +1476,8 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc) { + let default = AnimationsConfigIpc::default(); + let to_ipc = |c: AnimationCurve| match c { + AnimationCurve::Linear => AnimationCurveIpc::Linear, + AnimationCurve::EaseOut => AnimationCurveIpc::EaseOut, + AnimationCurve::EaseInOut => AnimationCurveIpc::EaseInOut, + AnimationCurve::Bezier { x1, y1, x2, y2 } => AnimationCurveIpc::Bezier { x1, y1, x2, y2 }, + }; + let cfg = match anim { + Some(a) => AnimationsConfigIpc { + enabled: a.enabled.unwrap_or(default.enabled), + open_duration_ms: a + .open_duration_ms + .unwrap_or(default.open_duration_ms) + .clamp(0, 10_000), + open_curve: to_ipc(a.open_curve.unwrap_or(AnimationCurve::EaseOut)), + close_duration_ms: a + .close_duration_ms + .unwrap_or(default.close_duration_ms) + .clamp(0, 10_000), + close_curve: to_ipc(a.close_curve.unwrap_or(AnimationCurve::EaseOut)), + }, + None => default, + }; + _set_animations_config(cfg); +} + fn push_blur_config(blur: Option) { let default = BlurConfigIpc::default(); let cfg = match blur { @@ -1732,6 +1766,19 @@ fn push_blur_config(blur: Option) { _set_blur_config(cfg); } +fn push_damage_visualization(dv: Option) { + let default = DamageVisualizationIpc::default(); + let cfg = match dv { + Some(d) => DamageVisualizationIpc { + enabled: d.enabled.unwrap_or(default.enabled), + color: d.color.unwrap_or(default.color), + decay_millis: d.decay_ms.unwrap_or(default.decay_millis), + }, + None => default, + }; + _set_damage_visualization(cfg); +} + fn push_layer_rules(rules: &[LayerRule]) { let ipc: Vec = rules .iter()