1
0
Fork 0
forked from wry/wry

Compare commits

...

4 commits

Author SHA1 Message Date
entailz
e35dce433a Add open and close animations, xdg_popup blur pre-pass,
damage viz config option.
2026-05-20 18:50:11 -07:00
entailz
12adb678bb accepts_input_at rejects buffer-less surfaces 2026-05-20 18:48:48 -07:00
entailz
bb43c238e3 Adds a discard_threshold push-constant to the tex / rounded_tex shaders 2026-05-20 18:47:16 -07:00
entailz
e2de688324 Replace the blit cascade in record_blur with shader-based dual-Kawase,
matching the OpenGL backend's algorithm. The blit cascade was a much weaker
   filter that degenerated badly on thin surfaces.

   The new path adds blur.vert / blur_down.frag (5-tap) / blur_up.frag (8-tap),
   a single-binding blur descriptor set layout, and per-format down/up pipelines.
   BLUR_SCRATCH_USAGE gains COLOR_ATTACHMENT so the scratch images can be both
   sampled and rendered into. Cache hit fast path and masked composite are
   unchanged.
2026-05-20 18:44:47 -07:00
53 changed files with 1849 additions and 267 deletions

View file

@ -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",

View file

@ -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,
}
}
}

View file

@ -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 })
}

View file

@ -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)]

View file

@ -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()

193
src/animation.rs Normal file
View file

@ -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<dyn GfxTexture>,
/// The output the toplevel was on, used to schedule per-frame damage.
pub output: Weak<OutputNode>,
/// 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<u64>,
}
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<f32> {
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<dyn ToplevelNode>) -> Option<Snapshot> {
let ctx = state.render_ctx.get()?;
let formats = ctx.formats();
let format_info = formats.get(&ARGB8888.drm)?;
let modifiers: Vec<Modifier> = 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()),
})
}

View file

@ -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);

View file

@ -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(())
}

View file

@ -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<BlurMask>,
pub cache: Option<Rc<std::cell::RefCell<Option<BlurCacheEntry>>>>,
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<dyn GfxTexture>,
}
#[derive(Clone)]
@ -120,6 +144,9 @@ pub struct BlurMask {
pub texture: Rc<dyn GfxTexture>,
pub source: SampleRect,
pub threshold: f32,
pub buffer_resv: Option<Rc<dyn BufferResv>>,
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,
}
}

View file

@ -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<Rc<VulkanImage>, 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()
{
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<Self>,
buf: CommandBuffer,
target: &VulkanImage,
rect: [i32; 4],
passes: u8,
offset: f32,
scratch_out: &mut Vec<Rc<VulkanImage>>,
mask: Option<&BlurMaskRecord<'_>>,
cached_blur: Option<&Rc<VulkanImage>>,
out_blur_image: &mut Option<Rc<VulkanImage>>,
) -> 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<Rc<VulkanImage>> = 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,121 +279,9 @@ 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));
unsafe {
dev.cmd_blit_image2(buf, &blit_info);
}
}
// Up passes: levels[i+1] -> levels[i] with linear filter.
for i in (0..passes as usize).rev() {
let (src, dst) = (&levels[i + 1], &levels[i]);
do_barriers(&[
barrier(
src.image,
ImageLayout::TRANSFER_DST_OPTIMAL,
ImageLayout::TRANSFER_SRC_OPTIMAL,
PipelineStageFlags2::TRANSFER,
AccessFlags2::TRANSFER_WRITE,
PipelineStageFlags2::TRANSFER,
AccessFlags2::TRANSFER_READ,
),
barrier(
dst.image,
ImageLayout::TRANSFER_SRC_OPTIMAL,
ImageLayout::TRANSFER_DST_OPTIMAL,
PipelineStageFlags2::TRANSFER,
AccessFlags2::TRANSFER_READ,
PipelineStageFlags2::TRANSFER,
AccessFlags2::TRANSFER_WRITE,
),
]);
let blit = ImageBlit2::default()
.src_subresource(subres)
.dst_subresource(subres)
.src_offsets([
Offset3D { x: 0, y: 0, z: 0 },
Offset3D {
x: src.width as i32,
y: src.height as i32,
z: 1,
},
])
.dst_offsets([
Offset3D { x: 0, y: 0, z: 0 },
Offset3D {
x: dst.width as i32,
y: dst.height as i32,
z: 1,
},
]);
let blit_info = BlitImageInfo2::default()
.src_image(src.image)
.src_image_layout(ImageLayout::TRANSFER_SRC_OPTIMAL)
.dst_image(dst.image)
.dst_image_layout(ImageLayout::TRANSFER_DST_OPTIMAL)
.filter(Filter::LINEAR)
.regions(slice::from_ref(&blit));
unsafe {
dev.cmd_blit_image2(buf, &blit_info);
}
}
if let Some(mask) = mask {
// Masked composite path:
// levels[0] (TRANSFER_DST) -> SHADER_READ_ONLY_OPTIMAL
// target (TRANSFER_SRC) -> COLOR_ATTACHMENT_OPTIMAL
// draw composite shader sampling levels[0] + mask, blending onto fb
do_barriers(&[
barrier(
// 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,
@ -374,8 +289,135 @@ impl VulkanRenderer {
AccessFlags2::TRANSFER_WRITE,
PipelineStageFlags2::FRAGMENT_SHADER,
AccessFlags2::SHADER_SAMPLED_READ,
),
barrier(
)]);
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_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] (SHADER_READ_ONLY) -> levels[i] (COLOR_ATT).
for i in (0..passes as usize).rev() {
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: 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,
@ -383,8 +425,7 @@ impl VulkanRenderer {
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,9 +558,11 @@ impl VulkanRenderer {
unsafe {
dev.cmd_blit_image2(buf, &blit_info);
}
// Restore target to COLOR_ATTACHMENT_OPTIMAL.
do_barriers(&[barrier(
// 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,
@ -526,11 +570,146 @@ impl VulkanRenderer {
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<Self>,
buf: CommandBuffer,
target: &VulkanImage,
cached: &Rc<VulkanImage>,
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(())
}
}

View file

@ -87,6 +87,31 @@ impl VulkanDevice {
}))
}
pub(super) fn create_blur_descriptor_set_layout(
self: &Rc<Self>,
sampler: &Rc<VulkanSampler>,
) -> Result<Rc<VulkanDescriptorSetLayout>, 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<Self>,
sampler: &Rc<VulkanSampler>,

View file

@ -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<VulkanShader>,
pub(super) blur_composite_descriptor_set_layout: Rc<VulkanDescriptorSetLayout>,
pub(super) blur_composite_pipelines: CopyHashMap<vk::Format, Rc<VulkanPipeline>>,
pub(super) blur_vert_shader: Rc<VulkanShader>,
pub(super) blur_down_frag_shader: Rc<VulkanShader>,
pub(super) blur_up_frag_shader: Rc<VulkanShader>,
pub(super) blur_descriptor_set_layout: Rc<VulkanDescriptorSetLayout>,
pub(super) blur_down_pipelines: CopyHashMap<vk::Format, Rc<VulkanPipeline>>,
pub(super) blur_up_pipelines: CopyHashMap<vk::Format, Rc<VulkanPipeline>>,
pub(super) defunct: Cell<bool>,
pub(super) pending_cpu_jobs: CopyHashMap<u64, PendingJob>,
pub(super) shm_allocator: Rc<VulkanThreadedAllocator>,
@ -229,13 +236,20 @@ enum VulkanOp {
struct VulkanBlurOp {
rect: crate::gfx_api::FramebufferRect,
passes: u8,
offset: f32,
mask: Option<VulkanBlurMask>,
cache: Option<Rc<RefCell<Option<crate::gfx_api::BlurCacheEntry>>>>,
cache_epoch: u64,
cache_pixel_rect: [i32; 4],
}
struct VulkanBlurMask {
tex: Rc<VulkanImage>,
source: crate::gfx_api::SampleRect,
threshold: f32,
buffer_resv: Option<Rc<dyn BufferResv>>,
acquire_sync: Option<AcquireSync>,
release_sync: ReleaseSync,
}
struct VulkanTexOp {
@ -246,6 +260,7 @@ struct VulkanTexOp {
acquire_sync: Option<AcquireSync>,
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<AcquireSync>,
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<Rc<VulkanPipeline>, 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<Rc<VulkanPipeline>, 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<VulkanShader>,
) -> Result<Rc<VulkanPipeline>, 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::<BlurPushConstants>() 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<Rc<VulkanImage>> = 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<Rc<VulkanImage>> = 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<Rect> = 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)

View file

@ -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<Self>,

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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) {

View file

@ -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;

View file

@ -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) {

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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

View file

@ -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();

View file

@ -318,7 +318,7 @@ pub struct WlSurface {
pub content_type: Cell<Option<ContentType>>,
pub drm_feedback: CopyHashMap<ZwpLinuxDmabufFeedbackV1Id, Rc<ZwpLinuxDmabufFeedbackV1>>,
syncobj_surface: CloneCell<Option<Rc<WpLinuxDrmSyncobjSurfaceV1>>>,
destroyed: Cell<bool>,
pub destroyed: Cell<bool>,
commit_timeline: CommitTimeline,
alpha_modifier: CloneCell<Option<Rc<WpAlphaModifierSurfaceV1>>>,
alpha: Cell<Option<f32>>,
@ -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;

View file

@ -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<bool>,
pub id: XdgPopupId,
node_id: PopupId,
pub xdg: Rc<XdgSurface>,
@ -79,6 +80,8 @@ pub struct XdgPopup {
relative_position: Cell<Rect>,
pos: RefCell<XdgPositioned>,
pub tracker: Tracker<Self>,
pub blur_cache: Rc<RefCell<Option<crate::gfx_api::BlurCacheEntry>>>,
pub blur_cache_epoch: NumCell<u64>,
seat_state: NodeSeatState,
set_visible_prepared: Cell<bool>,
jay_popup_ext: CloneCell<Option<Rc<JayPopupExtV1>>>,
@ -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<LayerPopupBlur> {
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 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);
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);
}
}

View file

@ -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());

View file

@ -53,6 +53,7 @@ pub struct ZwlrLayerSurfaceV1 {
pub client: Rc<Client>,
pub surface: Rc<WlSurface>,
pub output: Rc<OutputGlobalOpt>,
pub blur_cache_epoch: NumCell<u64>,
pub namespace: String,
pub tracker: Tracker<Self>,
output_extents: Cell<Rect>,
@ -62,6 +63,7 @@ pub struct ZwlrLayerSurfaceV1 {
pub blur: Cell<bool>,
pub blur_popups: Cell<bool>,
pub ignore_alpha: Cell<Option<f32>>,
pub blur_cache: Rc<RefCell<Option<crate::gfx_api::BlurCacheEntry>>>,
requested_serial: NumCell<u32>,
size: Cell<(i32, i32)>,
anchor: Cell<u32>,
@ -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),

View file

@ -48,6 +48,7 @@ mod leaks;
mod tracy;
mod acceptor;
mod allocator;
mod animation;
mod async_engine;
mod backend;
mod backends;

View file

@ -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<CornerRadius>,
/// 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<NodeId>,
}
#[must_use]
pub struct OpenAnimSaved {
alpha_mul: f32,
translate_x: f32,
translate_y: f32,
prev_node: Option<NodeId>,
}
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<dyn StackedNode> = 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<OpenAnimSaved> {
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<OpenAnimSaved>) {
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(

View file

@ -26,6 +26,9 @@ pub struct RendererBase<'a> {
pub fb_width: f32,
pub fb_height: f32,
pub discard_alpha: Option<f32>,
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<f32>) -> Option<f32> {
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<BlurMask>,
cache: Option<Rc<std::cell::RefCell<Option<crate::gfx_api::BlurCacheEntry>>>>,
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,
}));
}
}

View file

@ -306,6 +306,10 @@ pub struct State {
pub hyprland_global_shortcuts: CopyHashMap<(String, String), Rc<HyprlandGlobalShortcutV1>>,
pub layer_rules: RefCell<Vec<jay_config::_private::LayerRuleIpc>>,
pub blur_config: Cell<jay_config::_private::BlurConfigIpc>,
pub blur_cache_epoch: NumCell<u64>,
pub animations_config: Cell<jay_config::_private::AnimationsConfigIpc>,
pub active_animations: RefCell<Vec<std::rc::Weak<dyn ToplevelNode>>>,
pub close_snapshots: RefCell<Vec<Rc<crate::animation::Snapshot>>>,
}
// 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() {

View file

@ -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()
}

View file

@ -428,6 +428,7 @@ pub struct ToplevelData {
pub property_changed_source: OnceCell<Rc<LazyEventSource>>,
pub mapped_source: Rc<LazyEventSource>,
pub unmapped_source: Rc<LazyEventSource>,
pub anim_open_start_nsec: Cell<Option<u64>>,
}
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<f32> {
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<State>, tl: Rc<dyn ToplevelNode>, 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
}

View file

@ -393,6 +393,30 @@ pub struct BlurConfig {
pub size: Option<f32>,
}
#[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<bool>,
pub open_duration_ms: Option<u32>,
pub open_curve: Option<AnimationCurve>,
pub close_duration_ms: Option<u32>,
pub close_curve: Option<AnimationCurve>,
}
#[derive(Debug, Clone, Copy)]
pub struct DamageVisualization {
pub enabled: Option<bool>,
pub color: Option<jay_config::theme::Color>,
pub decay_ms: Option<u64>,
}
#[derive(Debug, Clone)]
pub enum DrmDeviceMatch {
Any(Vec<DrmDeviceMatch>),
@ -607,6 +631,8 @@ pub struct Config {
pub window_rules: Vec<WindowRule>,
pub layer_rules: Vec<LayerRule>,
pub blur: Option<BlurConfig>,
pub damage_visualization: Option<DamageVisualization>,
pub animations: Option<AnimationsConfig>,
pub pointer_revert_key: Option<KeySym>,
pub use_hardware_cursor: Option<bool>,
pub show_bar: Option<bool>,

View file

@ -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;

View file

@ -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<String>, Spanned<Value>>,
) -> ParseResult<Self> {
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<Spanned<&str>>| 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,
})
}
}

View file

@ -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(),

View file

@ -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<String>, Spanned<Value>>,
) -> ParseResult<Self> {
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,
})
}
}

View file

@ -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<Persistent
persistent.window_rules.set(window_rules);
push_layer_rules(&config.layer_rules);
push_blur_config(config.blur);
push_damage_visualization(config.damage_visualization);
push_animations_config(config.animations);
state.set_status(&config.status);
persistent.actions.borrow_mut().clear();
for a in config.named_actions {
@ -1720,6 +1727,33 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
}
}
fn push_animations_config(anim: Option<AnimationsConfig>) {
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<BlurConfig>) {
let default = BlurConfigIpc::default();
let cfg = match blur {
@ -1732,6 +1766,19 @@ fn push_blur_config(blur: Option<BlurConfig>) {
_set_blur_config(cfg);
}
fn push_damage_visualization(dv: Option<crate::config::DamageVisualization>) {
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<LayerRuleIpc> = rules
.iter()