1
0
Fork 0
forked from wry/wry

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.
This commit is contained in:
entailz 2026-05-20 18:44:47 -07:00
parent fa0ab2f1fa
commit e2de688324
12 changed files with 707 additions and 176 deletions

View file

@ -25,6 +25,9 @@ pub const TREES: &[Tree] = &[Tree {
"rounded_tex.vert", "rounded_tex.vert",
"blur_composite.vert", "blur_composite.vert",
"blur_composite.frag", "blur_composite.frag",
"blur.vert",
"blur_down.frag",
"blur_up.frag",
"legacy/fill.frag", "legacy/fill.frag",
"legacy/fill.vert", "legacy/fill.vert",
"legacy/tex.vert", "legacy/tex.vert",

View file

@ -2,8 +2,9 @@ use {
crate::gfx_apis::vulkan::{ crate::gfx_apis::vulkan::{
VulkanError, VulkanError,
image::{QueueFamily, QueueState, VulkanImage, VulkanImageMemory}, image::{QueueFamily, QueueState, VulkanImage, VulkanImageMemory},
pipeline::VulkanPipeline,
renderer::VulkanRenderer, renderer::VulkanRenderer,
shaders::BlurCompositePushConstants, shaders::{BlurCompositePushConstants, BlurPushConstants},
}, },
ash::vk::{ ash::vk::{
AccessFlags2, AttachmentLoadOp, AttachmentStoreOp, BlitImageInfo2, CommandBuffer, AccessFlags2, AttachmentLoadOp, AttachmentStoreOp, BlitImageInfo2, CommandBuffer,
@ -16,13 +17,14 @@ use {
}, },
gpu_alloc::UsageFlags, gpu_alloc::UsageFlags,
run_on_drop::on_drop, 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( const BLUR_SCRATCH_USAGE: ImageUsageFlags = ImageUsageFlags::from_raw(
ImageUsageFlags::TRANSFER_SRC.as_raw() ImageUsageFlags::TRANSFER_SRC.as_raw()
| ImageUsageFlags::TRANSFER_DST.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> { pub(super) struct BlurMaskRecord<'a> {
@ -42,12 +44,17 @@ impl VulkanRenderer {
) -> Result<Rc<VulkanImage>, VulkanError> { ) -> Result<Rc<VulkanImage>, VulkanError> {
let key = (width, height, format); let key = (width, height, format);
let cached = &mut *self.blur_scratch.borrow_mut(); let cached = &mut *self.blur_scratch.borrow_mut();
let entry = cached.entry(key);
if let Entry::Occupied(e) = &entry if let Some(weak) = cached.get(&key) {
&& let Some(img) = e.get().upgrade() if let Some(img) = weak.upgrade() {
{ if Rc::strong_count(&img) == 1 {
return Ok(img); img.is_undefined.set(false);
img.contents_are_undefined.set(false);
return Ok(img);
}
}
} }
let create_info = ImageCreateInfo::default() let create_info = ImageCreateInfo::default()
.image_type(ImageType::TYPE_2D) .image_type(ImageType::TYPE_2D)
.format(format) .format(format)
@ -77,7 +84,6 @@ impl VulkanRenderer {
.bind_image_memory(image, allocation.memory, allocation.offset) .bind_image_memory(image, allocation.memory, allocation.offset)
}; };
res.map_err(VulkanError::BindImageMemory)?; res.map_err(VulkanError::BindImageMemory)?;
// No view needed (we only blit), but VulkanImage requires one.
let image_view_create_info = ImageViewCreateInfo::default() let image_view_create_info = ImageViewCreateInfo::default()
.image(image) .image(image)
.format(format) .format(format)
@ -96,8 +102,6 @@ impl VulkanRenderer {
}; };
let view = view.map_err(VulkanError::CreateImageView)?; let view = view.map_err(VulkanError::CreateImageView)?;
destroy_image.forget(); 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 { let img = Rc::new(VulkanImage {
renderer: self.clone(), renderer: self.clone(),
format: crate::gfx_apis::vulkan::format::BLEND_FORMAT, format: crate::gfx_apis::vulkan::format::BLEND_FORMAT,
@ -117,28 +121,30 @@ impl VulkanRenderer {
sampled_image_descriptor: None, sampled_image_descriptor: None,
execution_version: Default::default(), execution_version: Default::default(),
}); });
match entry { cached.insert(key, Rc::downgrade(&img));
Entry::Occupied(mut e) => {
e.insert(Rc::downgrade(&img));
}
Entry::Vacant(e) => {
e.insert(Rc::downgrade(&img));
}
}
Ok(img) Ok(img)
} }
/// Records a backdrop blur of the given pixel rect on the target image. /// Records a dual-Kawase backdrop blur of the given pixel rect on the target
/// Caller is responsible for ending the current dynamic render pass before /// image. Caller is responsible for ending the current dynamic render pass
/// invoking, and for restarting it afterward (with LOAD). /// 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( pub(super) fn record_blur(
self: &Rc<Self>, self: &Rc<Self>,
buf: CommandBuffer, buf: CommandBuffer,
target: &VulkanImage, target: &VulkanImage,
rect: [i32; 4], rect: [i32; 4],
passes: u8, passes: u8,
offset: f32,
scratch_out: &mut Vec<Rc<VulkanImage>>, scratch_out: &mut Vec<Rc<VulkanImage>>,
mask: Option<&BlurMaskRecord<'_>>, mask: Option<&BlurMaskRecord<'_>>,
cached_blur: Option<&Rc<VulkanImage>>,
out_blur_image: &mut Option<Rc<VulkanImage>>,
) -> Result<(), VulkanError> { ) -> Result<(), VulkanError> {
let [x1, y1, x2, y2] = rect; let [x1, y1, x2, y2] = rect;
let x1 = x1.max(0).min(target.width as i32); let x1 = x1.max(0).min(target.width as i32);
@ -150,7 +156,26 @@ impl VulkanRenderer {
if w < 4 || h < 4 { if w < 4 || h < 4 {
return Ok(()); 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 format = target.format.vk_format;
let mut levels: Vec<Rc<VulkanImage>> = Vec::with_capacity(passes as usize + 1); 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)?); 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() let subres = ImageSubresourceLayers::default()
.aspect_mask(ImageAspectFlags::COLOR) .aspect_mask(ImageAspectFlags::COLOR)
.layer_count(1) .layer_count(1)
@ -203,7 +229,7 @@ impl VulkanRenderer {
}; };
// Step 1: target COLOR_ATTACHMENT -> TRANSFER_SRC. // Step 1: target COLOR_ATTACHMENT -> TRANSFER_SRC.
// Step 1: levels[0] UNDEFINED -> TRANSFER_DST. // levels[0] -> TRANSFER_DST (discard prior contents).
do_barriers(&[ do_barriers(&[
barrier( barrier(
target.image, target.image,
@ -224,6 +250,7 @@ impl VulkanRenderer {
AccessFlags2::TRANSFER_WRITE, AccessFlags2::TRANSFER_WRITE,
), ),
]); ]);
levels[0].is_undefined.set(false);
// Step 2: blit target rect -> levels[0] full. // Step 2: blit target rect -> levels[0] full.
let blit = ImageBlit2::default() let blit = ImageBlit2::default()
@ -252,139 +279,153 @@ impl VulkanRenderer {
dev.cmd_blit_image2(buf, &blit_info); dev.cmd_blit_image2(buf, &blit_info);
} }
// Down passes: levels[i-1] -> levels[i] with linear filter. // Step 3: levels[0] TRANSFER_DST -> SHADER_READ_ONLY for sampling in
for i in 1..=passes as usize { // the down pass.
let (src, dst) = (&levels[i - 1], &levels[i]); do_barriers(&[barrier(
do_barriers(&[ levels[0].image,
barrier( ImageLayout::TRANSFER_DST_OPTIMAL,
src.image, ImageLayout::SHADER_READ_ONLY_OPTIMAL,
ImageLayout::TRANSFER_DST_OPTIMAL, PipelineStageFlags2::TRANSFER,
ImageLayout::TRANSFER_SRC_OPTIMAL, AccessFlags2::TRANSFER_WRITE,
PipelineStageFlags2::TRANSFER, PipelineStageFlags2::FRAGMENT_SHADER,
AccessFlags2::TRANSFER_WRITE, AccessFlags2::SHADER_SAMPLED_READ,
PipelineStageFlags2::TRANSFER, )]);
AccessFlags2::TRANSFER_READ,
), let blur_down_pipeline = self.get_or_create_blur_down_pipeline(format)?;
barrier( let blur_up_pipeline = self.get_or_create_blur_up_pipeline(format)?;
dst.image,
ImageLayout::UNDEFINED, // Helper to run one blur pass: sample `src`, draw into `dst`. Caller
ImageLayout::TRANSFER_DST_OPTIMAL, // must have transitioned dst to COLOR_ATTACHMENT and src to
PipelineStageFlags2::TOP_OF_PIPE, // SHADER_READ_ONLY before this. Layouts after: dst stays in
AccessFlags2::empty(), // COLOR_ATTACHMENT (caller transitions next).
PipelineStageFlags2::TRANSFER, let run_pass = |pipeline: &VulkanPipeline,
AccessFlags2::TRANSFER_WRITE, src: &VulkanImage,
), dst: &VulkanImage|
]); -> Result<(), VulkanError> {
let blit = ImageBlit2::default() let color_attachment = RenderingAttachmentInfo::default()
.src_subresource(subres) .image_view(dst.texture_view)
.dst_subresource(subres) .image_layout(ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
.src_offsets([ .load_op(AttachmentLoadOp::DONT_CARE)
Offset3D { x: 0, y: 0, z: 0 }, .store_op(AttachmentStoreOp::STORE);
Offset3D { let render_area = Rect2D {
x: src.width as i32, offset: Offset2D { x: 0, y: 0 },
y: src.height as i32, extent: Extent2D {
z: 1, width: dst.width,
}, height: dst.height,
]) },
.dst_offsets([ };
Offset3D { x: 0, y: 0, z: 0 }, let rendering_info = RenderingInfo::default()
Offset3D { .render_area(render_area)
x: dst.width as i32, .layer_count(1)
y: dst.height as i32, .color_attachments(slice::from_ref(&color_attachment));
z: 1, let viewport = Viewport {
}, x: 0.0,
]); y: 0.0,
let blit_info = BlitImageInfo2::default() width: dst.width as f32,
.src_image(src.image) height: dst.height as f32,
.src_image_layout(ImageLayout::TRANSFER_SRC_OPTIMAL) min_depth: 0.0,
.dst_image(dst.image) max_depth: 1.0,
.dst_image_layout(ImageLayout::TRANSFER_DST_OPTIMAL) };
.filter(Filter::LINEAR) let scissor = render_area;
.regions(slice::from_ref(&blit)); 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 { unsafe {
dev.cmd_blit_image2(buf, &blit_info); dev.cmd_begin_rendering(buf, &rendering_info);
dev.cmd_bind_pipeline(buf, PipelineBindPoint::GRAPHICS, pipeline.pipeline);
dev.cmd_set_viewport(buf, 0, slice::from_ref(&viewport));
dev.cmd_set_scissor(buf, 0, slice::from_ref(&scissor));
self.device.push_descriptor.cmd_push_descriptor_set(
buf,
PipelineBindPoint::GRAPHICS,
pipeline.pipeline_layout,
0,
&writes,
);
dev.cmd_push_constants(
buf,
pipeline.pipeline_layout,
ShaderStageFlags::FRAGMENT,
0,
uapi::as_bytes(&push),
);
dev.cmd_draw(buf, 4, 1, 0, 0);
dev.cmd_end_rendering(buf);
} }
Ok(())
};
// Down passes: levels[i-1] (SHADER_READ_ONLY) -> levels[i] (COLOR_ATT).
// Each iteration transitions the destination to COLOR_ATTACHMENT,
// draws, then to SHADER_READ_ONLY for the next iteration's read.
for i in 1..=passes as usize {
do_barriers(&[barrier(
levels[i].image,
ImageLayout::UNDEFINED,
ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
PipelineStageFlags2::TOP_OF_PIPE,
AccessFlags2::empty(),
PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT,
AccessFlags2::COLOR_ATTACHMENT_WRITE,
)]);
levels[i].is_undefined.set(false);
run_pass(&blur_down_pipeline, &levels[i - 1], &levels[i])?;
do_barriers(&[barrier(
levels[i].image,
ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
ImageLayout::SHADER_READ_ONLY_OPTIMAL,
PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT,
AccessFlags2::COLOR_ATTACHMENT_WRITE,
PipelineStageFlags2::FRAGMENT_SHADER,
AccessFlags2::SHADER_SAMPLED_READ,
)]);
} }
// Up passes: levels[i+1] -> levels[i] with linear filter. // Up passes: levels[i+1] (SHADER_READ_ONLY) -> levels[i] (COLOR_ATT).
for i in (0..passes as usize).rev() { for i in (0..passes as usize).rev() {
let (src, dst) = (&levels[i + 1], &levels[i]); do_barriers(&[barrier(
do_barriers(&[ levels[i].image,
barrier( ImageLayout::SHADER_READ_ONLY_OPTIMAL,
src.image, ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
ImageLayout::TRANSFER_DST_OPTIMAL, PipelineStageFlags2::FRAGMENT_SHADER,
ImageLayout::TRANSFER_SRC_OPTIMAL, AccessFlags2::SHADER_SAMPLED_READ,
PipelineStageFlags2::TRANSFER, PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT,
AccessFlags2::TRANSFER_WRITE, AccessFlags2::COLOR_ATTACHMENT_WRITE,
PipelineStageFlags2::TRANSFER, )]);
AccessFlags2::TRANSFER_READ, run_pass(&blur_up_pipeline, &levels[i + 1], &levels[i])?;
), do_barriers(&[barrier(
barrier( levels[i].image,
dst.image, ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
ImageLayout::TRANSFER_SRC_OPTIMAL, ImageLayout::SHADER_READ_ONLY_OPTIMAL,
ImageLayout::TRANSFER_DST_OPTIMAL, PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT,
PipelineStageFlags2::TRANSFER, AccessFlags2::COLOR_ATTACHMENT_WRITE,
AccessFlags2::TRANSFER_READ, PipelineStageFlags2::FRAGMENT_SHADER,
PipelineStageFlags2::TRANSFER, AccessFlags2::SHADER_SAMPLED_READ,
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);
}
} }
// After cascade: levels[0] in SHADER_READ_ONLY, target in TRANSFER_SRC.
if let Some(mask) = mask { if let Some(mask) = mask {
// Masked composite path: // Masked composite path: restore target to COLOR_ATTACHMENT and
// levels[0] (TRANSFER_DST) -> SHADER_READ_ONLY_OPTIMAL // draw the composite shader sampling levels[0] + mask.
// target (TRANSFER_SRC) -> COLOR_ATTACHMENT_OPTIMAL do_barriers(&[barrier(
// draw composite shader sampling levels[0] + mask, blending onto fb target.image,
do_barriers(&[ ImageLayout::TRANSFER_SRC_OPTIMAL,
barrier( ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
levels[0].image, PipelineStageFlags2::TRANSFER,
ImageLayout::TRANSFER_DST_OPTIMAL, AccessFlags2::TRANSFER_READ,
ImageLayout::SHADER_READ_ONLY_OPTIMAL, PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT,
PipelineStageFlags2::TRANSFER, AccessFlags2::COLOR_ATTACHMENT_WRITE | AccessFlags2::COLOR_ATTACHMENT_READ,
AccessFlags2::TRANSFER_WRITE, )]);
PipelineStageFlags2::FRAGMENT_SHADER,
AccessFlags2::SHADER_SAMPLED_READ,
),
barrier(
target.image,
ImageLayout::TRANSFER_SRC_OPTIMAL,
ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
PipelineStageFlags2::TRANSFER,
AccessFlags2::TRANSFER_READ,
PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT,
AccessFlags2::COLOR_ATTACHMENT_WRITE | AccessFlags2::COLOR_ATTACHMENT_READ,
),
]);
let pipeline = self.get_or_create_blur_composite_pipeline(target.format.vk_format)?; let pipeline = self.get_or_create_blur_composite_pipeline(target.format.vk_format)?;
@ -470,14 +511,15 @@ impl VulkanRenderer {
dev.cmd_end_rendering(buf); dev.cmd_end_rendering(buf);
} }
} else { } 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(&[ do_barriers(&[
barrier( barrier(
levels[0].image, levels[0].image,
ImageLayout::TRANSFER_DST_OPTIMAL, ImageLayout::SHADER_READ_ONLY_OPTIMAL,
ImageLayout::TRANSFER_SRC_OPTIMAL, ImageLayout::TRANSFER_SRC_OPTIMAL,
PipelineStageFlags2::TRANSFER, PipelineStageFlags2::FRAGMENT_SHADER,
AccessFlags2::TRANSFER_WRITE, AccessFlags2::SHADER_SAMPLED_READ,
PipelineStageFlags2::TRANSFER, PipelineStageFlags2::TRANSFER,
AccessFlags2::TRANSFER_READ, AccessFlags2::TRANSFER_READ,
), ),
@ -516,21 +558,158 @@ impl VulkanRenderer {
unsafe { unsafe {
dev.cmd_blit_image2(buf, &blit_info); dev.cmd_blit_image2(buf, &blit_info);
} }
// Restore target to COLOR_ATTACHMENT for the resumed render pass.
// Restore target to COLOR_ATTACHMENT_OPTIMAL. // Also push levels[0] back to SHADER_READ_ONLY so its tracked layout
do_barriers(&[barrier( // matches what the cache-hit fast path expects on next frame.
target.image, do_barriers(&[
ImageLayout::TRANSFER_DST_OPTIMAL, barrier(
ImageLayout::COLOR_ATTACHMENT_OPTIMAL, target.image,
PipelineStageFlags2::TRANSFER, ImageLayout::TRANSFER_DST_OPTIMAL,
AccessFlags2::TRANSFER_WRITE, ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT, PipelineStageFlags2::TRANSFER,
AccessFlags2::COLOR_ATTACHMENT_WRITE | AccessFlags2::COLOR_ATTACHMENT_READ, 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. // Hold the scratch images until the frame is submitted.
scratch_out.extend(levels); scratch_out.extend(levels);
Ok(()) 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( pub(super) fn create_tex_sampler_descriptor_set_layout(
self: &Rc<Self>, self: &Rc<Self>,
sampler: &Rc<VulkanSampler>, sampler: &Rc<VulkanSampler>,

View file

@ -26,7 +26,8 @@ use {
sampler::VulkanSampler, sampler::VulkanSampler,
semaphore::VulkanSemaphore, semaphore::VulkanSemaphore,
shaders::{ 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, ColorManagementData, EotfArgs, FILL_FRAG, FILL_VERT, FillPushConstants,
InvEotfArgs, LEGACY_FILL_FRAG, LEGACY_FILL_VERT, LEGACY_ROUNDED_FILL_FRAG, InvEotfArgs, LEGACY_FILL_FRAG, LEGACY_FILL_VERT, LEGACY_ROUNDED_FILL_FRAG,
LEGACY_ROUNDED_FILL_VERT, LEGACY_ROUNDED_TEX_FRAG, LEGACY_ROUNDED_TEX_VERT, 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_frag_shader: Rc<VulkanShader>,
pub(super) blur_composite_descriptor_set_layout: Rc<VulkanDescriptorSetLayout>, pub(super) blur_composite_descriptor_set_layout: Rc<VulkanDescriptorSetLayout>,
pub(super) blur_composite_pipelines: CopyHashMap<vk::Format, Rc<VulkanPipeline>>, 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) defunct: Cell<bool>,
pub(super) pending_cpu_jobs: CopyHashMap<u64, PendingJob>, pub(super) pending_cpu_jobs: CopyHashMap<u64, PendingJob>,
pub(super) shm_allocator: Rc<VulkanThreadedAllocator>, pub(super) shm_allocator: Rc<VulkanThreadedAllocator>,
@ -229,13 +236,20 @@ enum VulkanOp {
struct VulkanBlurOp { struct VulkanBlurOp {
rect: crate::gfx_api::FramebufferRect, rect: crate::gfx_api::FramebufferRect,
passes: u8, passes: u8,
offset: f32,
mask: Option<VulkanBlurMask>, mask: Option<VulkanBlurMask>,
cache: Option<Rc<RefCell<Option<crate::gfx_api::BlurCacheEntry>>>>,
cache_epoch: u64,
cache_pixel_rect: [i32; 4],
} }
struct VulkanBlurMask { struct VulkanBlurMask {
tex: Rc<VulkanImage>, tex: Rc<VulkanImage>,
source: crate::gfx_api::SampleRect, source: crate::gfx_api::SampleRect,
threshold: f32, threshold: f32,
buffer_resv: Option<Rc<dyn BufferResv>>,
acquire_sync: Option<AcquireSync>,
release_sync: ReleaseSync,
} }
struct VulkanTexOp { struct VulkanTexOp {
@ -246,6 +260,7 @@ struct VulkanTexOp {
acquire_sync: Option<AcquireSync>, acquire_sync: Option<AcquireSync>,
release_sync: ReleaseSync, release_sync: ReleaseSync,
alpha: f32, alpha: f32,
discard_alpha: f32,
source_type: TexSourceType, source_type: TexSourceType,
copy_type: TexCopyType, copy_type: TexCopyType,
alpha_mode: AlphaMode, alpha_mode: AlphaMode,
@ -286,6 +301,7 @@ struct VulkanRoundedTexOp {
acquire_sync: Option<AcquireSync>, acquire_sync: Option<AcquireSync>,
release_sync: ReleaseSync, release_sync: ReleaseSync,
alpha: f32, alpha: f32,
discard_alpha: f32,
source_type: TexSourceType, source_type: TexSourceType,
copy_type: TexCopyType, copy_type: TexCopyType,
alpha_mode: AlphaMode, alpha_mode: AlphaMode,
@ -406,6 +422,10 @@ impl VulkanDevice {
let blur_composite_frag_shader = self.create_shader(BLUR_COMPOSITE_FRAG)?; let blur_composite_frag_shader = self.create_shader(BLUR_COMPOSITE_FRAG)?;
let blur_composite_descriptor_set_layout = let blur_composite_descriptor_set_layout =
self.create_blur_composite_descriptor_set_layout(&sampler)?; 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 gfx_command_buffers = self.create_command_pool(self.graphics_queue_idx)?;
let transfer_command_buffers = self let transfer_command_buffers = self
.distinct_transfer_queue_family_idx .distinct_transfer_queue_family_idx
@ -497,6 +517,12 @@ impl VulkanDevice {
blur_composite_frag_shader, blur_composite_frag_shader,
blur_composite_descriptor_set_layout, blur_composite_descriptor_set_layout,
blur_composite_pipelines: Default::default(), 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), defunct: Cell::new(false),
pending_cpu_jobs: Default::default(), pending_cpu_jobs: Default::default(),
shm_allocator, 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 { pub(super) fn allocate_point(&self) -> u64 {
self.last_point.fetch_add(1) + 1 self.last_point.fetch_add(1) + 1
} }
@ -1263,6 +1411,7 @@ impl VulkanRenderer {
acquire_sync: Some(ct.acquire_sync.clone()), acquire_sync: Some(ct.acquire_sync.clone()),
release_sync: ct.release_sync, release_sync: ct.release_sync,
alpha: ct.alpha.unwrap_or_default(), alpha: ct.alpha.unwrap_or_default(),
discard_alpha: ct.discard_alpha.unwrap_or(-1.0),
source_type, source_type,
copy_type, copy_type,
alpha_mode: ct.alpha_mode, alpha_mode: ct.alpha_mode,
@ -1361,6 +1510,7 @@ impl VulkanRenderer {
acquire_sync: Some(ct.acquire_sync.clone()), acquire_sync: Some(ct.acquire_sync.clone()),
release_sync: ct.release_sync, release_sync: ct.release_sync,
alpha: ct.alpha.unwrap_or_default(), alpha: ct.alpha.unwrap_or_default(),
discard_alpha: ct.discard_alpha.unwrap_or(-1.0),
source_type, source_type,
copy_type, copy_type,
alpha_mode: ct.alpha_mode, alpha_mode: ct.alpha_mode,
@ -1376,9 +1526,6 @@ impl VulkanRenderer {
} }
} }
GfxApiOpt::BlurBackdrop(b) => { 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); sync(memory);
let mask = if let Some(m) = &b.mask { let mask = if let Some(m) = &b.mask {
let tex = m.texture.clone().into_vk(&self.device.device)?; let tex = m.texture.clone().into_vk(&self.device.device)?;
@ -1393,15 +1540,35 @@ impl VulkanRenderer {
tex, tex,
source: m.source, source: m.source,
threshold: m.threshold, threshold: m.threshold,
buffer_resv: m.buffer_resv.clone(),
acquire_sync: Some(m.acquire_sync.clone()),
release_sync: m.release_sync,
}) })
} }
} else { } else {
None 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, rect: b.rect,
passes: b.passes, passes: b.passes,
offset: b.offset,
mask, 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, release_sync,
}); });
} else if let VulkanOp::BlurBarrier(b) = cmd } else if let VulkanOp::BlurBarrier(b) = cmd
&& let Some(m) = &b.mask && let Some(m) = &mut b.mask
{ {
let tex = &m.tex; let tex = &m.tex;
if tex.execution_version.replace(execution) != execution { if tex.execution_version.replace(execution) != execution {
@ -1598,6 +1765,12 @@ impl VulkanRenderer {
if let VulkanImageMemory::DmaBuf(_) = &tex.ty { if let VulkanImageMemory::DmaBuf(_) = &tex.ty {
memory.dmabuf_sample.push(tex.clone()) 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 { let push = TexPushConstants {
vertices: c.range_address, vertices: c.range_address,
alpha: c.alpha, alpha: c.alpha,
discard_threshold: c.discard_alpha,
}; };
unsafe { unsafe {
db.cmd_set_descriptor_buffer_offsets( db.cmd_set_descriptor_buffer_offsets(
@ -1966,6 +2140,7 @@ impl VulkanRenderer {
pos, pos,
tex_pos, tex_pos,
alpha: c.alpha, alpha: c.alpha,
discard_threshold: c.discard_alpha,
}; };
unsafe { unsafe {
dev.cmd_push_constants( dev.cmd_push_constants(
@ -2045,6 +2220,7 @@ impl VulkanRenderer {
let push = RoundedTexPushConstants { let push = RoundedTexPushConstants {
vertices: c.range_address, vertices: c.range_address,
alpha: c.alpha, alpha: c.alpha,
discard_threshold: c.discard_alpha,
size_x: c.size[0], size_x: c.size[0],
size_y: c.size[1], size_y: c.size[1],
corner_radius_tl: c.corner_radius[0], corner_radius_tl: c.corner_radius[0],
@ -2088,6 +2264,7 @@ impl VulkanRenderer {
pos: c.target, pos: c.target,
tex_pos: c.source, tex_pos: c.source,
alpha: c.alpha, alpha: c.alpha,
discard_threshold: c.discard_alpha,
size_x: c.size[0], size_x: c.size[0],
size_y: c.size[1], size_y: c.size[1],
corner_radius_tl: c.corner_radius[0], corner_radius_tl: c.corner_radius[0],
@ -2109,10 +2286,8 @@ impl VulkanRenderer {
} }
} }
VulkanOp::BlurBarrier(blur) => { VulkanOp::BlurBarrier(blur) => {
// Blur is only meaningful in the FrameBuffer pass. // BlurBarrier is pushed to exactly one pass in convert_ops
if pass != RenderPass::FrameBuffer { // (BB if present, else FB), so no per-pass gating is needed.
continue;
}
// End the current dynamic render pass, run the blur work // End the current dynamic render pass, run the blur work
// (image-blit cascade between scratch images), and resume // (image-blit cascade between scratch images), and resume
// the render pass with LOAD so subsequent draws layer on // the render pass with LOAD so subsequent draws layer on
@ -2132,14 +2307,55 @@ impl VulkanRenderer {
threshold: m.threshold, threshold: m.threshold,
_phantom: std::marker::PhantomData, _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( self.record_blur(
buf, buf,
target, target,
rect_arr, rect_arr,
blur.passes, blur.passes,
blur.offset,
&mut local_blur_scratch, &mut local_blur_scratch,
mask_record.as_ref(), 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); self.begin_rendering_load(buf, target);
// Pipeline state is invalidated across the render-pass // Pipeline state is invalidated across the render-pass
// break — force re-bind on next draw. // break — force re-bind on next draw.
@ -2679,6 +2895,7 @@ impl VulkanRenderer {
let width = fb.width as f32; let width = fb.width as f32;
let height = fb.height as f32; let height = fb.height as f32;
let mut tag = 0; let mut tag = 0;
let mut blur_rects: Vec<Rect> = Vec::new();
for opt in opts.iter().rev() { for opt in opts.iter().rev() {
let (opaque, fb_rect) = match opt { let (opaque, fb_rect) = match opt {
GfxApiOpt::Sync => continue, GfxApiOpt::Sync => continue,
@ -2706,7 +2923,10 @@ impl VulkanRenderer {
(false, rf.rect) (false, rf.rect)
} }
GfxApiOpt::RoundedCopyTexture(ct) => (false, ct.target), 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() { if opaque || bb.is_none() {
tag |= 1; tag |= 1;
@ -2719,6 +2939,20 @@ impl VulkanRenderer {
} }
memory.regions_2.push(rect.with_tag(tag)); 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 clear_region = if clear.is_some() {
let opaque_region = Region::from_rects2(&memory.regions_1); let opaque_region = Region::from_rects2(&memory.regions_1);
region.subtract_cow(&opaque_region) 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 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_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_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] = pub const LEGACY_ROUNDED_FILL_VERT: &[u8] =
include_bytes!("shaders_bin/legacy_rounded_fill.vert.spv"); include_bytes!("shaders_bin/legacy_rounded_fill.vert.spv");
pub const LEGACY_ROUNDED_FILL_FRAG: &[u8] = pub const LEGACY_ROUNDED_FILL_FRAG: &[u8] =
@ -69,6 +72,7 @@ unsafe impl Packed for TexVertex {}
pub struct TexPushConstants { pub struct TexPushConstants {
pub vertices: DeviceAddress, pub vertices: DeviceAddress,
pub alpha: f32, pub alpha: f32,
pub discard_threshold: f32,
} }
unsafe impl Packed for TexPushConstants {} unsafe impl Packed for TexPushConstants {}
@ -109,6 +113,7 @@ pub struct LegacyTexPushConstants {
pub pos: [[f32; 2]; 4], pub pos: [[f32; 2]; 4],
pub tex_pos: [[f32; 2]; 4], pub tex_pos: [[f32; 2]; 4],
pub alpha: f32, pub alpha: f32,
pub discard_threshold: f32,
} }
unsafe impl Packed for LegacyTexPushConstants {} unsafe impl Packed for LegacyTexPushConstants {}
@ -148,6 +153,7 @@ unsafe impl Packed for LegacyRoundedFillPushConstants {}
pub struct RoundedTexPushConstants { pub struct RoundedTexPushConstants {
pub vertices: DeviceAddress, pub vertices: DeviceAddress,
pub alpha: f32, pub alpha: f32,
pub discard_threshold: f32,
pub size_x: f32, pub size_x: f32,
pub size_y: f32, pub size_y: f32,
pub corner_radius_tl: f32, pub corner_radius_tl: f32,
@ -165,6 +171,7 @@ pub struct LegacyRoundedTexPushConstants {
pub pos: [[f32; 2]; 4], pub pos: [[f32; 2]; 4],
pub tex_pos: [[f32; 2]; 4], pub tex_pos: [[f32; 2]; 4],
pub alpha: f32, pub alpha: f32,
pub discard_threshold: f32,
pub size_x: f32, pub size_x: f32,
pub size_y: f32, pub size_y: f32,
pub corner_radius_tl: f32, pub corner_radius_tl: f32,
@ -195,6 +202,15 @@ pub struct BlurCompositePushConstants {
unsafe impl Packed for 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 { impl VulkanDevice {
pub(super) fn create_shader( pub(super) fn create_shader(
self: &Rc<Self>, 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;
}

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 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 16ad6f1eb029ccce5e0204a7d79709b05a8a708133feaf8bb20a24371de25ed7 src/gfx_apis/vulkan/shaders/blur_composite.frag
6399e23afa2e07c98b9fd1a4e853ea974a9958547ce65734846483bd7cbc8461 src/gfx_apis/vulkan/shaders/blur_composite.vert 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 b6a0df1e231fab533499329636b7a580384784418baee06c147af5fcc384cf5c src/gfx_apis/vulkan/shaders/eotfs.glsl
8a38df18851cd13884499820f26939fb7319f45d913d867f254d8118d59fb117 src/gfx_apis/vulkan/shaders/fill.common.glsl 8a38df18851cd13884499820f26939fb7319f45d913d867f254d8118d59fb117 src/gfx_apis/vulkan/shaders/fill.common.glsl
21c488d12aa5ad2f109ec44cb856dfe837e02ea9025b5ed64439d742c17cbf30 src/gfx_apis/vulkan/shaders/fill.frag 21c488d12aa5ad2f109ec44cb856dfe837e02ea9025b5ed64439d742c17cbf30 src/gfx_apis/vulkan/shaders/fill.frag