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/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/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/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/blur.vert.spv b/src/gfx_apis/vulkan/shaders_bin/blur.vert.spv new file mode 100644 index 00000000..6b57ef62 Binary files /dev/null and b/src/gfx_apis/vulkan/shaders_bin/blur.vert.spv differ diff --git a/src/gfx_apis/vulkan/shaders_bin/blur_down.frag.spv b/src/gfx_apis/vulkan/shaders_bin/blur_down.frag.spv new file mode 100644 index 00000000..d455ae52 Binary files /dev/null and b/src/gfx_apis/vulkan/shaders_bin/blur_down.frag.spv differ 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 00000000..56b6fe73 Binary files /dev/null and b/src/gfx_apis/vulkan/shaders_bin/blur_up.frag.spv differ 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 9fd78f9b..300db829 100644 Binary files a/src/gfx_apis/vulkan/shaders_bin/legacy_rounded_tex.frag.spv and b/src/gfx_apis/vulkan/shaders_bin/legacy_rounded_tex.frag.spv differ diff --git a/src/gfx_apis/vulkan/shaders_bin/legacy_rounded_tex.vert.spv b/src/gfx_apis/vulkan/shaders_bin/legacy_rounded_tex.vert.spv index 0a0d73e2..c296a7ec 100644 Binary files a/src/gfx_apis/vulkan/shaders_bin/legacy_rounded_tex.vert.spv and b/src/gfx_apis/vulkan/shaders_bin/legacy_rounded_tex.vert.spv differ diff --git a/src/gfx_apis/vulkan/shaders_bin/legacy_tex.frag.spv b/src/gfx_apis/vulkan/shaders_bin/legacy_tex.frag.spv index 9664f044..669b3064 100644 Binary files a/src/gfx_apis/vulkan/shaders_bin/legacy_tex.frag.spv and b/src/gfx_apis/vulkan/shaders_bin/legacy_tex.frag.spv differ 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 edf86be6..7361189a 100644 Binary files a/src/gfx_apis/vulkan/shaders_bin/legacy_tex.vert.spv and b/src/gfx_apis/vulkan/shaders_bin/legacy_tex.vert.spv differ 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 2bdd1fd3..3ff5c280 100644 Binary files a/src/gfx_apis/vulkan/shaders_bin/rounded_tex.frag.spv and b/src/gfx_apis/vulkan/shaders_bin/rounded_tex.frag.spv differ diff --git a/src/gfx_apis/vulkan/shaders_bin/rounded_tex.vert.spv b/src/gfx_apis/vulkan/shaders_bin/rounded_tex.vert.spv index 3ac5e409..7e11b397 100644 Binary files a/src/gfx_apis/vulkan/shaders_bin/rounded_tex.vert.spv and b/src/gfx_apis/vulkan/shaders_bin/rounded_tex.vert.spv differ diff --git a/src/gfx_apis/vulkan/shaders_bin/tex.frag.spv b/src/gfx_apis/vulkan/shaders_bin/tex.frag.spv index c013804d..f1ca6188 100644 Binary files a/src/gfx_apis/vulkan/shaders_bin/tex.frag.spv and b/src/gfx_apis/vulkan/shaders_bin/tex.frag.spv differ diff --git a/src/gfx_apis/vulkan/shaders_bin/tex.vert.spv b/src/gfx_apis/vulkan/shaders_bin/tex.vert.spv index 74f97643..00f1aa5a 100644 Binary files a/src/gfx_apis/vulkan/shaders_bin/tex.vert.spv and b/src/gfx_apis/vulkan/shaders_bin/tex.vert.spv differ diff --git a/src/gfx_apis/vulkan/shaders_hash.txt b/src/gfx_apis/vulkan/shaders_hash.txt index de378df5..f8b7f0e8 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 @@ -12,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 @@ -24,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 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.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; 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()