git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/vk/renderer.rs (136.9K)

   1 //! The ash renderer. One graphics queue, a classic render pass, two frames in
   2 //! flight, FIFO (vsync) presentation. Memory goes through gpu-allocator; the
   3 //! descriptor set mirrors `shader.wgsl`'s @group(0): sampled backdrop texture
   4 //! (binding 0), sampler (binding 1), WindowInfo uniform (binding 2). Binding 0
   5 //! is the scene backdrop until the first blur-behind plate: there the UI pass
   6 //! suspends, the frame-so-far is copied into the snapshot image, and the
   7 //! snapshot descriptor set takes over — so blur plates blur everything painted
   8 //! beneath them, not just the 3D scene.
   9 
  10 use std::ffi::c_void;
  11 
  12 use ash::vk;
  13 use gpu_allocator::vulkan::{
  14     Allocation, AllocationCreateDesc, AllocationScheme, Allocator,
  15 };
  16 use gpu_allocator::MemoryLocation;
  17 
  18 use crate::engine::Vertex;
  19 
  20 use super::core::SurfaceLost;
  21 use super::image::ImageStage;
  22 pub use crate::draw::{Batch2D, Frame2D, PlatePush, MAX_PLATE_FEATURES};
  23 pub(crate) use crate::draw::{batch_push_constants, PUSH_CONSTANT_FLOATS};
  24 use super::rt::{PreparedRtScene, RtCamera, RtEnvironment, RtImage, RtImageSource, RtMaterial, RtStage, RtTriangle};
  25 use super::scene::{MeshId, SceneDraw, SceneImage, SceneStage, Vertex3D};
  26 use super::text::{TextSpan, TextStage};
  27 
  28 /// The block in bytes. **This is exactly `maxPushConstantsSize`'s
  29 /// Vulkan-guaranteed minimum, so the budget is full** — every one of the 32
  30 /// slots is written. That is why a new SDF mode reinterprets existing fields per
  31 /// mode (5 reads `p_rect` as centre + radius, 6/7 as centre + radius + wedge
  32 /// angle, 8 as centre + half-width with `p_radii.xy` a normal) instead of adding
  33 /// one: there is nothing left to add.
  34 ///
  35 /// A block over 128 bytes is not portable by construction — 128 is the floor
  36 /// every conformant implementation must offer, and plenty of drivers offer no
  37 /// more. So growing this means querying `limits.max_push_constants_size` at
  38 /// device init and having a real fallback (a uniform buffer, or splitting the
  39 /// block), not just raising the number. The assertion below is the tripwire: a
  40 /// runtime check would be dead code today, because at exactly 128 it can never
  41 /// fire on a conformant device.
  42 pub(crate) const PUSH_CONSTANT_BYTES: u32 = (PUSH_CONSTANT_FLOATS * 4) as u32;
  43 
  44 const _: () = assert!(
  45     PUSH_CONSTANT_BYTES <= 128,
  46     "the push-constant block has outgrown the 128-byte Vulkan-guaranteed minimum: \
  47      query limits.max_push_constants_size at device init and add a fallback path \
  48      before raising PUSH_CONSTANT_FLOATS"
  49 );
  50 
  51 /// How far a swapchain image's pixels are behind the latest frame. A frame
  52 /// with [`Frame2D::damage`] repaints only what the image it acquired is
  53 /// missing — the frame's own damage plus whatever frames that went to the
  54 /// OTHER images changed in the meantime — instead of every pixel.
  55 #[derive(Debug, Clone, Copy, PartialEq)]
  56 enum ImageAge {
  57     /// Never rendered, or a full-surface frame went by: repaint everything.
  58     Unknown,
  59     /// Holds the latest frame.
  60     Current,
  61     /// Holds the latest frame except inside this rect.
  62     Behind(vk::Rect2D),
  63 }
  64 
  65 fn rect_union(a: vk::Rect2D, b: vk::Rect2D) -> vk::Rect2D {
  66     if a.extent.width == 0 || a.extent.height == 0 {
  67         return b;
  68     }
  69     if b.extent.width == 0 || b.extent.height == 0 {
  70         return a;
  71     }
  72     let x0 = a.offset.x.min(b.offset.x);
  73     let y0 = a.offset.y.min(b.offset.y);
  74     let x1 = (a.offset.x + a.extent.width as i32).max(b.offset.x + b.extent.width as i32);
  75     let y1 = (a.offset.y + a.extent.height as i32).max(b.offset.y + b.extent.height as i32);
  76     vk::Rect2D {
  77         offset: vk::Offset2D { x: x0, y: y0 },
  78         extent: vk::Extent2D { width: (x1 - x0) as u32, height: (y1 - y0) as u32 },
  79     }
  80 }
  81 
  82 /// `a` cut down to `b`; zero-sized (a legal scissor that draws nothing) when
  83 /// they do not meet.
  84 fn rect_intersect(a: vk::Rect2D, b: vk::Rect2D) -> vk::Rect2D {
  85     let x0 = a.offset.x.max(b.offset.x);
  86     let y0 = a.offset.y.max(b.offset.y);
  87     let x1 = (a.offset.x + a.extent.width as i32).min(b.offset.x + b.extent.width as i32);
  88     let y1 = (a.offset.y + a.extent.height as i32).min(b.offset.y + b.extent.height as i32);
  89     vk::Rect2D {
  90         offset: vk::Offset2D { x: x0, y: y0 },
  91         extent: vk::Extent2D {
  92             width: (x1 - x0).max(0) as u32,
  93             height: (y1 - y0).max(0) as u32,
  94         },
  95     }
  96 }
  97 
  98 /// How far beyond its own rect a frosted plate's blur can sample, physical
  99 /// px: shader2d's 7x7 kernel reaches three taps of the plate's stride either
 100 /// way, plus the rim's refraction offset. Generous on purpose — the panel's
 101 /// stride is 5.5 px at scale 2, so the real reach is ~17 px — because an
 102 /// under-estimate shows as a seam and an over-estimate only repaints more.
 103 const BLUR_REACH_PX: i32 = 96;
 104 
 105 fn rect_expand(r: vk::Rect2D, by: i32) -> vk::Rect2D {
 106     vk::Rect2D {
 107         offset: vk::Offset2D { x: r.offset.x - by, y: r.offset.y - by },
 108         extent: vk::Extent2D { width: r.extent.width + 2 * by as u32, height: r.extent.height + 2 * by as u32 },
 109     }
 110 }
 111 
 112 fn rect_overlaps(a: vk::Rect2D, b: vk::Rect2D) -> bool {
 113     let i = rect_intersect(a, b);
 114     i.extent.width > 0 && i.extent.height > 0
 115 }
 116 
 117 /// The physical-px box a run of vertices covers (positions are NDC, y up).
 118 pub(crate) fn verts_bounds(verts: &[crate::engine::Vertex], extent: vk::Extent2D) -> Option<vk::Rect2D> {
 119     let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
 120     for v in verts {
 121         let px = (v.position[0] + 1.0) * 0.5 * extent.width as f32;
 122         let py = (1.0 - v.position[1]) * 0.5 * extent.height as f32;
 123         x0 = x0.min(px);
 124         y0 = y0.min(py);
 125         x1 = x1.max(px);
 126         y1 = y1.max(py);
 127     }
 128     if x0 > x1 {
 129         return None;
 130     }
 131     let (x0, y0) = ((x0.floor() - 1.0) as i32, (y0.floor() - 1.0) as i32);
 132     let (x1, y1) = ((x1.ceil() + 1.0) as i32, (y1.ceil() + 1.0) as i32);
 133     Some(vk::Rect2D {
 134         offset: vk::Offset2D { x: x0, y: y0 },
 135         extent: vk::Extent2D { width: (x1 - x0).max(0) as u32, height: (y1 - y0).max(0) as u32 },
 136     })
 137 }
 138 
 139 /// Index of the frosted batch that is the first thing the frame draws, if
 140 /// it may sample the zeroed scene backdrop instead of a snapshot of the
 141 /// frame so far: with nothing drawn yet, a transparent clear and no scene in
 142 /// the backdrop, the two hold the same pixels — and the backdrop needs no
 143 /// copy and, in a partial frame, does not depend on pixels outside the
 144 /// repainted region. That is what lets the root plate, frosted in every
 145 /// themed app and the size of the window, stay out of the region growth
 146 /// below.
 147 pub(crate) fn first_frost_exempt(batches: &[Batch2D], images: &[crate::draw::ImageQuad], clear: [f32; 4], use_backdrop: bool) -> Option<usize> {
 148     if use_backdrop || clear != [0.0, 0.0, 0.0, 0.0] {
 149         return None;
 150     }
 151     let first = batches.iter().position(|b| b.start < b.end)?;
 152     let batch = &batches[first];
 153     let image_before = images.iter().any(|q| q.z_before <= batch.start);
 154     (batch.blur_behind && !image_before).then_some(first)
 155 }
 156 
 157 pub(crate) const FRAMES_IN_FLIGHT: usize = 2;
 158 pub(crate) use crate::draw::PLATE_FEATURE_BYTES;
 159 /// shader2d's WindowInfo uniform, in bytes (layout in `draw::window_info_data`).
 160 pub(crate) const WINDOW_INFO_BYTES: vk::DeviceSize = crate::draw::WINDOW_INFO_BYTES as vk::DeviceSize;
 161 pub(crate) use crate::draw::relief_px_at;
 162 
 163 /// [`crate::draw::window_info_data`] for a Vulkan extent.
 164 pub(crate) fn window_info_data(extent: vk::Extent2D, clip_corner_radius: f32, relief: (f32, f32)) -> [f32; crate::draw::WINDOW_INFO_BYTES / 4] {
 165     crate::draw::window_info_data(extent.width, extent.height, clip_corner_radius, relief)
 166 }
 167 
 168 pub(crate) struct AllocatedBuffer {
 169     pub(crate) buffer: vk::Buffer,
 170     pub(crate) allocation: Option<Allocation>,
 171     pub(crate) size: vk::DeviceSize,
 172 }
 173 
 174 impl AllocatedBuffer {
 175     pub(crate) fn null() -> Self {
 176         AllocatedBuffer { buffer: vk::Buffer::null(), allocation: None, size: 0 }
 177     }
 178 }
 179 
 180 /// Create a host-visible buffer bound to gpu-allocator memory.
 181 pub(crate) fn create_cpu_buffer(
 182     device: &ash::Device,
 183     allocator: &mut Allocator,
 184     size: vk::DeviceSize,
 185     usage: vk::BufferUsageFlags,
 186     name: &str,
 187 ) -> AllocatedBuffer {
 188     unsafe {
 189         let buffer = device
 190             .create_buffer(
 191                 &vk::BufferCreateInfo::default()
 192                     .size(size)
 193                     .usage(usage)
 194                     .sharing_mode(vk::SharingMode::EXCLUSIVE),
 195                 None,
 196             )
 197             .expect("Failed to create buffer");
 198         let requirements = device.get_buffer_memory_requirements(buffer);
 199         let allocation = allocator
 200             .allocate(&AllocationCreateDesc {
 201                 name,
 202                 requirements,
 203                 location: MemoryLocation::CpuToGpu,
 204                 linear: true,
 205                 allocation_scheme: AllocationScheme::GpuAllocatorManaged,
 206             })
 207             .expect("Failed to allocate buffer memory");
 208         device
 209             .bind_buffer_memory(buffer, allocation.memory(), allocation.offset())
 210             .expect("Failed to bind buffer memory");
 211         AllocatedBuffer { buffer, allocation: Some(allocation), size }
 212     }
 213 }
 214 
 215 /// Destroy a buffer and return its memory to the allocator.
 216 pub(crate) fn destroy_cpu_buffer(
 217     device: &ash::Device,
 218     allocator: &mut Allocator,
 219     buf: &mut AllocatedBuffer,
 220 ) {
 221     unsafe {
 222         self::destroy_buffer_handle(device, buf.buffer);
 223     }
 224     if let Some(allocation) = buf.allocation.take() {
 225         let _ = allocator.free(allocation);
 226     }
 227     buf.buffer = vk::Buffer::null();
 228     buf.size = 0;
 229 }
 230 
 231 unsafe fn destroy_buffer_handle(device: &ash::Device, buffer: vk::Buffer) {
 232     if buffer != vk::Buffer::null() {
 233         device.destroy_buffer(buffer, None);
 234     }
 235 }
 236 
 237 struct Frame {
 238     cmd: vk::CommandBuffer,
 239     image_available: vk::Semaphore,
 240     in_flight: vk::Fence,
 241     vertex: AllocatedBuffer,
 242     vertex_count: u32,
 243     overlay_start: u32,
 244     overlay_count: u32,
 245 }
 246 
 247 pub struct VkRenderer {
 248     surface: vk::SurfaceKHR,
 249 
 250     swapchain_loader: ash::khr::swapchain::Device,
 251     swapchain: vk::SwapchainKHR,
 252     surface_format: vk::SurfaceFormatKHR,
 253     extent: vk::Extent2D,
 254     swapchain_images: Vec<vk::Image>,
 255     swapchain_views: Vec<vk::ImageView>,
 256     framebuffers: Vec<vk::Framebuffer>,
 257     // One per swapchain image (not per frame in flight): present waits on the
 258     // semaphore tied to the image being presented.
 259     render_finished: Vec<vk::Semaphore>,
 260 
 261     render_pass: vk::RenderPass,
 262     /// UI pass over a backdrop copy: loadOp LOAD, initial layout TRANSFER_DST.
 263     /// Framebuffers are shared with `render_pass` (compatible attachments).
 264     render_pass_load: vk::RenderPass,
 265     /// LOAD from PRESENT_SRC: a partial frame repaints inside a swapchain
 266     /// image that still holds an earlier frame.
 267     render_pass_partial: vk::RenderPass,
 268     /// Per swapchain image, what it is missing; reset with the swapchain.
 269     image_ages: Vec<ImageAge>,
 270     descriptor_set_layout: vk::DescriptorSetLayout,
 271     pipeline_layout: vk::PipelineLayout,
 272     pipeline: vk::Pipeline,
 273     shader_module: vk::ShaderModule,
 274 
 275     descriptor_pool: vk::DescriptorPool,
 276     descriptor_set: vk::DescriptorSet,
 277     /// Twin of `descriptor_set` with binding 0 pointing at `snapshot_image`
 278     /// instead of the scene backdrop; bound for every draw after the first
 279     /// mid-pass snapshot so blur plates sample the frame-so-far.
 280     descriptor_set_snapshot: vk::DescriptorSet,
 281     /// Mid-frame copy target for blur-behind plates: the swapchain content so
 282     /// far, sampled by the resumed pass's blur draws. Sized with the surface.
 283     snapshot_image: vk::Image,
 284     snapshot_view: vk::ImageView,
 285     snapshot_allocation: Option<Allocation>,
 286     /// Whether a frame has needed the blur snapshot; until one has, it is
 287     /// not allocated (`sync_snapshot_target`).
 288     snapshot_wanted: bool,
 289     /// The snapshot's mip levels: 1 where the surface format cannot be
 290     /// blitted with a linear filter (`blur_mips`), else
 291     /// [`snapshot_levels`] of the surface. See `snapshot_mip_chain`.
 292     snapshot_levels: u32,
 293     /// Whether the surface format can be a blit's source and destination
 294     /// and filter linearly — what building the snapshot's mip chain takes.
 295     blur_mips: bool,
 296     /// Ask for the surface's minimum image count rather than one more
 297     /// (`set_minimal_swapchain`).
 298     minimal_swapchain: bool,
 299     backdrop_sampler: vk::Sampler,
 300     window_info: AllocatedBuffer,
 301     /// The bevel-profile generation `window_info` was last written with —
 302     /// `draw_frame_2d` rewrites the UBO when the layout global moves on.
 303     profile_gen: u64,
 304     /// The pinned relief heights (carve drop, roll rise) in physical px as
 305     /// last uploaded in WindowInfo — compared each frame, since editors set
 306     /// them straight into the style registry with no generation counter.
 307     relief_uploaded: (f32, f32),
 308     /// Same for the edge (roll) profile LUT.
 309     roll_profile_gen: u64,
 310     plate_features: AllocatedBuffer,
 311 
 312     frames: Vec<Frame>,
 313     frame_index: usize,
 314     text: TextStage,
 315     scene: SceneStage,
 316     image: ImageStage,
 317     /// Built lazily on the first `set_rt_scene`, so ordinary UI apps never
 318     /// compile the path-tracer pipeline.
 319     rt: Option<RtStage>,
 320     /// See [`VkRenderer::set_rt_background`].
 321     rt_background: Option<[f32; 3]>,
 322     /// See [`VkRenderer::set_rt_environment`].
 323     rt_environment: RtEnvironment,
 324 
 325     desired_extent: vk::Extent2D,
 326     corner_radius_px: f32,
 327     swapchain_dirty: bool,
 328     present_mode: vk::PresentModeKHR,
 329     present_debug_count: u64,
 330     /// Set when a surface call reports the surface lost (see [`SurfaceLost`]):
 331     /// the display connection is dead, so every later draw is skipped until
 332     /// a new surface is attached, rather than re-failing (and re-logging)
 333     /// each frame while the caller's event loop finds out for itself.
 334     surface_lost: bool,
 335 
 336     // Declared last: everything above must be destroyed before the device/
 337     // instance the core tears down in its own Drop.
 338     core: super::core::VkCore,
 339 }
 340 
 341 /// Compile WGSL to SPIR-V. The Y-flip between wgpu NDC (Y-up) and Vulkan NDC
 342 /// (Y-down) is handled with a negative-height viewport (like wgpu-hal), NOT in
 343 /// the shader — flipping in the shader would reverse screen-space winding and
 344 /// break the 3D pipeline's back-face culling.
 345 /// The 2D UI pipeline over `render_pass`: shader2d's descriptor set layout,
 346 /// its push-constant range, the vertex layout and the blend — what the
 347 /// renderer draws every frame with. Shared with the offscreen test harness
 348 /// (`vk::plate_probe`), so it draws with exactly the live pipeline.
 349 pub(crate) unsafe fn create_ui_pipeline(
 350     device: &ash::Device,
 351     render_pass: vk::RenderPass,
 352 ) -> (vk::DescriptorSetLayout, vk::PipelineLayout, vk::ShaderModule, vk::Pipeline) {
 353     // Descriptor set layout mirroring shader.wgsl @group(0): naga maps WGSL
 354     // texture/sampler/uniform bindings 1:1 onto set 0 descriptor bindings.
 355     let bindings = [
 356         vk::DescriptorSetLayoutBinding::default()
 357             .binding(0)
 358             .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
 359             .descriptor_count(1)
 360             .stage_flags(vk::ShaderStageFlags::FRAGMENT),
 361         vk::DescriptorSetLayoutBinding::default()
 362             .binding(1)
 363             .descriptor_type(vk::DescriptorType::SAMPLER)
 364             .descriptor_count(1)
 365             .stage_flags(vk::ShaderStageFlags::FRAGMENT),
 366         vk::DescriptorSetLayoutBinding::default()
 367             .binding(2)
 368             .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
 369             .descriptor_count(1)
 370             .stage_flags(vk::ShaderStageFlags::FRAGMENT),
 371         vk::DescriptorSetLayoutBinding::default()
 372             .binding(3)
 373             .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
 374             .descriptor_count(1)
 375             .stage_flags(vk::ShaderStageFlags::FRAGMENT),
 376     ];
 377     let descriptor_set_layout = device
 378         .create_descriptor_set_layout(
 379             &vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
 380             None,
 381         )
 382         .expect("Failed to create descriptor set layout");
 383 
 384     let set_layouts = [descriptor_set_layout];
 385     // Push constants: the per-batch rounded-rect clip plus the SDF-lit
 386     // plate block (eight vec4s, matching shader2d's `RRectClip`), read by
 387     // shader2d's fragment stage. See `PUSH_CONSTANT_BYTES` — the block is
 388     // exactly the Vulkan-guaranteed minimum and completely full.
 389     let push_ranges = [vk::PushConstantRange::default()
 390         .stage_flags(vk::ShaderStageFlags::FRAGMENT)
 391         .offset(0)
 392         .size(PUSH_CONSTANT_BYTES)];
 393     let pipeline_layout = device
 394         .create_pipeline_layout(
 395             &vk::PipelineLayoutCreateInfo::default()
 396                 .set_layouts(&set_layouts)
 397                 .push_constant_ranges(&push_ranges),
 398             None,
 399         )
 400         .expect("Failed to create pipeline layout");
 401 
 402     // Pipeline from shader.wgsl (both entry points live in one SPIR-V module).
 403     let spirv = shader2d_spirv();
 404     let shader_module = device
 405         .create_shader_module(&vk::ShaderModuleCreateInfo::default().code(spirv), None)
 406         .expect("Failed to create shader module");
 407 
 408     let stages = [
 409         vk::PipelineShaderStageCreateInfo::default()
 410             .stage(vk::ShaderStageFlags::VERTEX)
 411             .module(shader_module)
 412             .name(c"vs_main"),
 413         vk::PipelineShaderStageCreateInfo::default()
 414             .stage(vk::ShaderStageFlags::FRAGMENT)
 415             .module(shader_module)
 416             .name(c"fs_main"),
 417     ];
 418 
 419     // Vertex layout = cce_ui::engine::Vertex: pos vec2f, color vec4f, clip vec3f.
 420     let vertex_bindings = [vk::VertexInputBindingDescription::default()
 421         .binding(0)
 422         .stride(std::mem::size_of::<Vertex>() as u32)
 423         .input_rate(vk::VertexInputRate::VERTEX)];
 424     let vertex_attributes = [
 425         vk::VertexInputAttributeDescription::default()
 426             .location(0)
 427             .binding(0)
 428             .format(vk::Format::R32G32_SFLOAT)
 429             .offset(0),
 430         vk::VertexInputAttributeDescription::default()
 431             .location(1)
 432             .binding(0)
 433             .format(vk::Format::R32G32B32A32_SFLOAT)
 434             .offset(8),
 435         vk::VertexInputAttributeDescription::default()
 436             .location(2)
 437             .binding(0)
 438             .format(vk::Format::R32G32B32_SFLOAT)
 439             .offset(24),
 440     ];
 441     let vertex_input = vk::PipelineVertexInputStateCreateInfo::default()
 442         .vertex_binding_descriptions(&vertex_bindings)
 443         .vertex_attribute_descriptions(&vertex_attributes);
 444 
 445     let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::default()
 446         .topology(vk::PrimitiveTopology::TRIANGLE_LIST);
 447     let viewport_state = vk::PipelineViewportStateCreateInfo::default()
 448         .viewport_count(1)
 449         .scissor_count(1);
 450     let rasterization = vk::PipelineRasterizationStateCreateInfo::default()
 451         .polygon_mode(vk::PolygonMode::FILL)
 452         .cull_mode(vk::CullModeFlags::NONE)
 453         .front_face(vk::FrontFace::COUNTER_CLOCKWISE)
 454         .line_width(1.0);
 455     let multisample = vk::PipelineMultisampleStateCreateInfo::default()
 456         .rasterization_samples(vk::SampleCountFlags::TYPE_1);
 457     // wgpu::BlendState::ALPHA_BLENDING.
 458     let blend_attachments = [vk::PipelineColorBlendAttachmentState::default()
 459         .blend_enable(true)
 460         .src_color_blend_factor(vk::BlendFactor::SRC_ALPHA)
 461         .dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
 462         .color_blend_op(vk::BlendOp::ADD)
 463         .src_alpha_blend_factor(vk::BlendFactor::ONE)
 464         .dst_alpha_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
 465         .alpha_blend_op(vk::BlendOp::ADD)
 466         .color_write_mask(vk::ColorComponentFlags::RGBA)];
 467     let color_blend =
 468         vk::PipelineColorBlendStateCreateInfo::default().attachments(&blend_attachments);
 469     let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR];
 470     let dynamic_state =
 471         vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
 472 
 473     let pipeline = device
 474         .create_graphics_pipelines(
 475             vk::PipelineCache::null(),
 476             &[vk::GraphicsPipelineCreateInfo::default()
 477                 .stages(&stages)
 478                 .vertex_input_state(&vertex_input)
 479                 .input_assembly_state(&input_assembly)
 480                 .viewport_state(&viewport_state)
 481                 .rasterization_state(&rasterization)
 482                 .multisample_state(&multisample)
 483                 .color_blend_state(&color_blend)
 484                 .dynamic_state(&dynamic_state)
 485                 .layout(pipeline_layout)
 486                 .render_pass(render_pass)
 487                 .subpass(0)],
 488             None,
 489         )
 490         .expect("Failed to create graphics pipeline")[0];
 491     (descriptor_set_layout, pipeline_layout, shader_module, pipeline)
 492 }
 493 
 494 pub(crate) fn compile_wgsl(source: &str) -> Vec<u32> {
 495     let module = naga::front::wgsl::parse_str(source).expect("WGSL parse failed");
 496     let info = naga::valid::Validator::new(
 497         naga::valid::ValidationFlags::all(),
 498         naga::valid::Capabilities::PUSH_CONSTANT,
 499     )
 500     .validate(&module)
 501     .expect("WGSL validation failed");
 502     let options = naga::back::spv::Options {
 503         lang_version: (1, 0),
 504         flags: naga::back::spv::WriterFlags::LABEL_VARYINGS,
 505         ..Default::default()
 506     };
 507     naga::back::spv::write_vec(&module, &info, &options, None).expect("SPIR-V write failed")
 508 }
 509 
 510 /// SPIR-V for the renderer's fixed shaders, compiled from their WGSL by
 511 /// `build.rs` and embedded. naga used to compile them in every process that
 512 /// built a renderer, 20-60 ms of each app's launch (nearly all shader2d).
 513 /// Only the byte-to-word conversion runs here, once per process.
 514 macro_rules! precompiled_spirv {
 515     ($name:ident, $file:literal) => {
 516         pub(crate) fn $name() -> &'static [u32] {
 517             static SPIRV: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
 518             SPIRV.get_or_init(|| {
 519                 include_bytes!(concat!(env!("OUT_DIR"), "/", $file))
 520                     .chunks_exact(4)
 521                     .map(|w| u32::from_le_bytes([w[0], w[1], w[2], w[3]]))
 522                     .collect()
 523             })
 524         }
 525     };
 526 }
 527 
 528 precompiled_spirv!(shader2d_spirv, "shader2d.spv");
 529 precompiled_spirv!(glyph_spirv, "glyph.spv");
 530 precompiled_spirv!(scene3d_spirv, "scene3d.spv");
 531 precompiled_spirv!(scene3d_image_spirv, "scene3d_image.spv");
 532 precompiled_spirv!(scene3d_lit_spirv, "scene3d_lit.spv");
 533 
 534 /// Like [`compile_wgsl`], but with naga's RAY_QUERY capability and SPIR-V 1.4
 535 /// (required by SPV_KHR_ray_query). Only used on devices where the ray-query
 536 /// device stack was enabled — those are Vulkan 1.2+, which accepts 1.4.
 537 pub(crate) fn compile_wgsl_ray_query(source: &str) -> Vec<u32> {
 538     let module = naga::front::wgsl::parse_str(source).expect("WGSL parse failed");
 539     let info = naga::valid::Validator::new(
 540         naga::valid::ValidationFlags::all(),
 541         naga::valid::Capabilities::RAY_QUERY,
 542     )
 543     .validate(&module)
 544     .expect("WGSL validation failed");
 545     let options = naga::back::spv::Options {
 546         lang_version: (1, 4),
 547         flags: naga::back::spv::WriterFlags::LABEL_VARYINGS,
 548         ..Default::default()
 549     };
 550     naga::back::spv::write_vec(&module, &info, &options, None).expect("SPIR-V write failed")
 551 }
 552 
 553 const COLOR_RANGE: vk::ImageSubresourceRange = color_levels(0, 1);
 554 
 555 /// `count` mip levels of a colour image from `base`.
 556 const fn color_levels(base: u32, count: u32) -> vk::ImageSubresourceRange {
 557     vk::ImageSubresourceRange {
 558         aspect_mask: vk::ImageAspectFlags::COLOR,
 559         base_mip_level: base,
 560         level_count: count,
 561         base_array_layer: 0,
 562         layer_count: 1,
 563     }
 564 }
 565 
 566 /// How many mip levels the blur snapshot carries: enough that the coarsest
 567 /// texel is as wide as the widest kernel stride a plate asks for (a radius of
 568 /// 16 px at scale 4 is a 64 px stride, level 6), and no more — the levels
 569 /// past that would be built every snapshot and never read. See
 570 /// `snapshot_mip_chain`.
 571 pub(crate) const SNAPSHOT_LEVELS_MAX: u32 = 7;
 572 
 573 /// The levels a snapshot of `extent` can have, at most [`SNAPSHOT_LEVELS_MAX`]:
 574 /// a level stops halving at one texel.
 575 pub(crate) fn snapshot_levels(extent: vk::Extent2D) -> u32 {
 576     let side = extent.width.max(extent.height).max(1);
 577     (32 - side.leading_zeros()).min(SNAPSHOT_LEVELS_MAX)
 578 }
 579 
 580 /// One-time submit: clear a color image and leave it in SHADER_READ_ONLY, so a
 581 /// freshly created backdrop is always legal to sample.
 582 pub(crate) fn clear_image_to_shader_read(
 583     device: &ash::Device,
 584     queue: vk::Queue,
 585     command_pool: vk::CommandPool,
 586     image: vk::Image,
 587 ) {
 588     clear_image_levels_to_shader_read(device, queue, command_pool, image, 1);
 589 }
 590 
 591 /// [`clear_image_to_shader_read`] over the first `levels` mip levels.
 592 pub(crate) fn clear_image_levels_to_shader_read(
 593     device: &ash::Device,
 594     queue: vk::Queue,
 595     command_pool: vk::CommandPool,
 596     image: vk::Image,
 597     levels: u32,
 598 ) {
 599     let range = color_levels(0, levels);
 600     unsafe {
 601         let cmd = device
 602             .allocate_command_buffers(
 603                 &vk::CommandBufferAllocateInfo::default()
 604                     .command_pool(command_pool)
 605                     .level(vk::CommandBufferLevel::PRIMARY)
 606                     .command_buffer_count(1),
 607             )
 608             .expect("Failed to allocate init command buffer")[0];
 609         device
 610             .begin_command_buffer(
 611                 cmd,
 612                 &vk::CommandBufferBeginInfo::default()
 613                     .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
 614             )
 615             .unwrap();
 616         device.cmd_pipeline_barrier(
 617             cmd,
 618             vk::PipelineStageFlags::TOP_OF_PIPE,
 619             vk::PipelineStageFlags::TRANSFER,
 620             vk::DependencyFlags::empty(),
 621             &[],
 622             &[],
 623             &[vk::ImageMemoryBarrier::default()
 624                 .src_access_mask(vk::AccessFlags::empty())
 625                 .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
 626                 .old_layout(vk::ImageLayout::UNDEFINED)
 627                 .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
 628                 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
 629                 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
 630                 .image(image)
 631                 .subresource_range(range)],
 632         );
 633         device.cmd_clear_color_image(
 634             cmd,
 635             image,
 636             vk::ImageLayout::TRANSFER_DST_OPTIMAL,
 637             &vk::ClearColorValue { float32: [0.0, 0.0, 0.0, 0.0] },
 638             &[range],
 639         );
 640         device.cmd_pipeline_barrier(
 641             cmd,
 642             vk::PipelineStageFlags::TRANSFER,
 643             vk::PipelineStageFlags::FRAGMENT_SHADER,
 644             vk::DependencyFlags::empty(),
 645             &[],
 646             &[],
 647             &[vk::ImageMemoryBarrier::default()
 648                 .src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
 649                 .dst_access_mask(vk::AccessFlags::SHADER_READ)
 650                 .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
 651                 .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
 652                 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
 653                 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
 654                 .image(image)
 655                 .subresource_range(range)],
 656         );
 657         device.end_command_buffer(cmd).unwrap();
 658         let cmds = [cmd];
 659         let submit = vk::SubmitInfo::default().command_buffers(&cmds);
 660         device
 661             .queue_submit(queue, &[submit], vk::Fence::null())
 662             .expect("Init submit failed");
 663         device.queue_wait_idle(queue).expect("Init wait failed");
 664         device.free_command_buffers(command_pool, &cmds);
 665     }
 666 }
 667 
 668 /// The wgpu-convention viewport: Y flipped via negative height (Vulkan >= 1.1).
 669 pub(crate) fn flipped_viewport(extent: vk::Extent2D) -> vk::Viewport {
 670     vk::Viewport {
 671         x: 0.0,
 672         y: extent.height as f32,
 673         width: extent.width as f32,
 674         height: -(extent.height as f32),
 675         min_depth: 0.0,
 676         max_depth: 1.0,
 677     }
 678 }
 679 
 680 
 681 impl VkRenderer {
 682     /// A renderer presenting to `surface_ptr`, or [`SurfaceLost`] when the
 683     /// display connection under it is already dead — which is what a window
 684     /// requested as the compositor goes away gets. The caller should treat
 685     /// that as its connection ending (the runner does), not retry here.
 686     ///
 687     /// # Safety
 688     /// `display_ptr` and `surface_ptr` must be live `wl_display` / `wl_surface`
 689     /// pointers that outlive the renderer.
 690     pub unsafe fn try_new(
 691         display_ptr: *mut c_void,
 692         surface_ptr: *mut c_void,
 693         width: u32,
 694         height: u32,
 695         corner_radius_px: f32,
 696     ) -> Result<Self, SurfaceLost> {
 697         Self::try_new_for(
 698             super::core::SurfaceTarget::Wayland { display: display_ptr, surface: surface_ptr },
 699             width,
 700             height,
 701             corner_radius_px,
 702         )
 703     }
 704 
 705     /// A renderer presenting to any window [`SurfaceTarget`](super::core::SurfaceTarget)
 706     /// names — a Wayland surface, or on macOS a `CAMetalLayer` — on the same
 707     /// terms as [`try_new`](Self::try_new).
 708     ///
 709     /// # Safety
 710     /// The target's pointers must be live and outlive the renderer.
 711     pub unsafe fn try_new_for(
 712         target: super::core::SurfaceTarget,
 713         width: u32,
 714         height: u32,
 715         corner_radius_px: f32,
 716     ) -> Result<Self, SurfaceLost> {
 717         let t_new = std::time::Instant::now();
 718         let (mut core, surface) = super::core::VkCore::new_for_surface(target)?;
 719         log::debug!("[timing] VkCore::new_for_surface: {:?}", t_new.elapsed());
 720         let t_rest = std::time::Instant::now();
 721         // Locals over the core for the setup below (methods use self.core.*).
 722         let device = core.device.clone();
 723         let queue = core.queue;
 724         let command_pool = core.command_pool;
 725         let physical_device = core.physical_device;
 726         let min_uniform_align = core.min_uniform_align;
 727         let surface_loader = core.surface_loader.clone();
 728 
 729         // Surface format: prefer sRGB (wgpu's get_default_config sorts sRGB first,
 730         // so this matches the colors the app renders today). The first query
 731         // that talks to the compositor, so the one a dead connection fails.
 732         let formats = match surface_loader
 733             .get_physical_device_surface_formats(physical_device, surface)
 734         {
 735             Ok(formats) if !formats.is_empty() => formats,
 736             Ok(_) => panic!("surface offers no formats"),
 737             Err(result) => {
 738                 surface_loader.destroy_surface(surface, None);
 739                 return Err(SurfaceLost { call: "vkGetPhysicalDeviceSurfaceFormatsKHR", result });
 740             }
 741         };
 742         let allocator = core.allocator.as_mut().unwrap();
 743         let surface_format = formats
 744             .iter()
 745             .copied()
 746             .find(|f| {
 747                 (f.format == vk::Format::B8G8R8A8_SRGB || f.format == vk::Format::R8G8B8A8_SRGB)
 748                     && f.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR
 749             })
 750             .unwrap_or(formats[0]);
 751 
 752         // Render pass: one color attachment, clear -> present.
 753         let attachments = [vk::AttachmentDescription::default()
 754             .format(surface_format.format)
 755             .samples(vk::SampleCountFlags::TYPE_1)
 756             .load_op(vk::AttachmentLoadOp::CLEAR)
 757             .store_op(vk::AttachmentStoreOp::STORE)
 758             .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
 759             .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
 760             .initial_layout(vk::ImageLayout::UNDEFINED)
 761             .final_layout(vk::ImageLayout::PRESENT_SRC_KHR)];
 762         let color_refs = [vk::AttachmentReference::default()
 763             .attachment(0)
 764             .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)];
 765         let subpasses = [vk::SubpassDescription::default()
 766             .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
 767             .color_attachments(&color_refs)];
 768         // One dependency shared VERBATIM by both UI pass variants: framebuffer
 769         // compatibility requires identical dependencies (only load/store ops and
 770         // image layouts may differ), so this unions the clear case (previous
 771         // frame's color output) with the load case (the backdrop copy's write).
 772         let dependencies = [vk::SubpassDependency::default()
 773             .src_subpass(vk::SUBPASS_EXTERNAL)
 774             .dst_subpass(0)
 775             .src_stage_mask(
 776                 vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
 777                     | vk::PipelineStageFlags::TRANSFER,
 778             )
 779             .src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
 780             .dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
 781             .dst_access_mask(
 782                 vk::AccessFlags::COLOR_ATTACHMENT_READ | vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
 783             )];
 784         let render_pass = device
 785             .create_render_pass(
 786                 &vk::RenderPassCreateInfo::default()
 787                     .attachments(&attachments)
 788                     .subpasses(&subpasses)
 789                     .dependencies(&dependencies),
 790                 None,
 791             )
 792             .expect("Failed to create render pass");
 793 
 794         // Variant used when a backdrop copy precedes the UI pass: keep the copied
 795         // pixels (LOAD) and take the image from the copy's TRANSFER_DST layout.
 796         let attachments_load = [vk::AttachmentDescription::default()
 797             .format(surface_format.format)
 798             .samples(vk::SampleCountFlags::TYPE_1)
 799             .load_op(vk::AttachmentLoadOp::LOAD)
 800             .store_op(vk::AttachmentStoreOp::STORE)
 801             .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
 802             .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
 803             .initial_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
 804             .final_layout(vk::ImageLayout::PRESENT_SRC_KHR)];
 805         let render_pass_load = device
 806             .create_render_pass(
 807                 &vk::RenderPassCreateInfo::default()
 808                     .attachments(&attachments_load)
 809                     .subpasses(&subpasses)
 810                     .dependencies(&dependencies),
 811                 None,
 812             )
 813             .expect("Failed to create load render pass");
 814 
 815         // Variant for a partial frame: keep what the image already shows and
 816         // repaint inside the damage only. A presented image comes back from
 817         // acquire in PRESENT_SRC with its contents intact.
 818         let attachments_partial = [vk::AttachmentDescription::default()
 819             .format(surface_format.format)
 820             .samples(vk::SampleCountFlags::TYPE_1)
 821             .load_op(vk::AttachmentLoadOp::LOAD)
 822             .store_op(vk::AttachmentStoreOp::STORE)
 823             .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
 824             .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
 825             .initial_layout(vk::ImageLayout::PRESENT_SRC_KHR)
 826             .final_layout(vk::ImageLayout::PRESENT_SRC_KHR)];
 827         let render_pass_partial = device
 828             .create_render_pass(
 829                 &vk::RenderPassCreateInfo::default()
 830                     .attachments(&attachments_partial)
 831                     .subpasses(&subpasses)
 832                     .dependencies(&dependencies),
 833                 None,
 834             )
 835             .expect("Failed to create partial render pass");
 836 
 837         let (descriptor_set_layout, pipeline_layout, shader_module, pipeline) =
 838             create_ui_pipeline(&device, render_pass);
 839 
 840         // Full-size backdrop + depth live in the scene stage: the 3D pass renders
 841         // into the backdrop, and the UI pass samples it for blur-behind plates.
 842         let initial_extent = vk::Extent2D { width: width.max(1), height: height.max(1) };
 843         let scene = SceneStage::new(
 844             &device,
 845             allocator,
 846             surface_format.format,
 847             initial_extent,
 848             FRAMES_IN_FLIGHT,
 849             min_uniform_align,
 850             core.max_line_width,
 851         );
 852         clear_image_to_shader_read(&device, queue, command_pool, scene.backdrop_image);
 853 
 854         // Linear, clamp-to-edge, and linear BETWEEN mip levels: the blur
 855         // snapshot carries a mip chain and the frost kernel reads it at a
 856         // fractional level (shader2d's `resolve_blur`). The scene backdrop has
 857         // one level, which every level clamps to.
 858         let backdrop_sampler = device
 859             .create_sampler(
 860                 &vk::SamplerCreateInfo::default()
 861                     .mag_filter(vk::Filter::LINEAR)
 862                     .min_filter(vk::Filter::LINEAR)
 863                     .mipmap_mode(vk::SamplerMipmapMode::LINEAR)
 864                     .max_lod(vk::LOD_CLAMP_NONE)
 865                     .address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE)
 866                     .address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE)
 867                     .address_mode_w(vk::SamplerAddressMode::CLAMP_TO_EDGE),
 868                 None,
 869             )
 870             .expect("Failed to create sampler");
 871 
 872         let window_info = create_cpu_buffer(
 873             &device,
 874             allocator,
 875             WINDOW_INFO_BYTES,
 876             vk::BufferUsageFlags::UNIFORM_BUFFER,
 877             "window-info",
 878         );
 879         // Plate-carve features, one MAX_PLATE_FEATURES slot per frame in
 880         // flight so a write never races the previous frame's reads.
 881         let plate_features = create_cpu_buffer(
 882             &device,
 883             allocator,
 884             (FRAMES_IN_FLIGHT * MAX_PLATE_FEATURES * PLATE_FEATURE_BYTES) as vk::DeviceSize,
 885             vk::BufferUsageFlags::UNIFORM_BUFFER,
 886             "plate-features",
 887         );
 888 
 889         // Two sets: the scene-backdrop set and its snapshot twin (binding 0
 890         // differs; 1-3 alias the same sampler/uniforms).
 891         let pool_sizes = [
 892             vk::DescriptorPoolSize::default()
 893                 .ty(vk::DescriptorType::SAMPLED_IMAGE)
 894                 .descriptor_count(2),
 895             vk::DescriptorPoolSize::default()
 896                 .ty(vk::DescriptorType::SAMPLER)
 897                 .descriptor_count(2),
 898             vk::DescriptorPoolSize::default()
 899                 .ty(vk::DescriptorType::UNIFORM_BUFFER)
 900                 .descriptor_count(4),
 901         ];
 902         let descriptor_pool = device
 903             .create_descriptor_pool(
 904                 &vk::DescriptorPoolCreateInfo::default()
 905                     .max_sets(2)
 906                     .pool_sizes(&pool_sizes),
 907                 None,
 908             )
 909             .expect("Failed to create descriptor pool");
 910         let both_layouts = [descriptor_set_layout, descriptor_set_layout];
 911         let sets = device
 912             .allocate_descriptor_sets(
 913                 &vk::DescriptorSetAllocateInfo::default()
 914                     .descriptor_pool(descriptor_pool)
 915                     .set_layouts(&both_layouts),
 916             )
 917             .expect("Failed to allocate descriptor sets");
 918         let (descriptor_set, descriptor_set_snapshot) = (sets[0], sets[1]);
 919 
 920         let image_infos = [vk::DescriptorImageInfo::default()
 921             .image_view(scene.backdrop_view)
 922             .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)];
 923         let sampler_infos = [vk::DescriptorImageInfo::default().sampler(backdrop_sampler)];
 924         let buffer_infos = [vk::DescriptorBufferInfo::default()
 925             .buffer(window_info.buffer)
 926             .offset(0)
 927             .range(WINDOW_INFO_BYTES)];
 928         let feature_infos = [vk::DescriptorBufferInfo::default()
 929             .buffer(plate_features.buffer)
 930             .offset(0)
 931             .range((FRAMES_IN_FLIGHT * MAX_PLATE_FEATURES * PLATE_FEATURE_BYTES) as vk::DeviceSize)];
 932         device.update_descriptor_sets(
 933             &[
 934                 vk::WriteDescriptorSet::default()
 935                     .dst_set(descriptor_set)
 936                     .dst_binding(0)
 937                     .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
 938                     .image_info(&image_infos),
 939                 vk::WriteDescriptorSet::default()
 940                     .dst_set(descriptor_set)
 941                     .dst_binding(1)
 942                     .descriptor_type(vk::DescriptorType::SAMPLER)
 943                     .image_info(&sampler_infos),
 944                 vk::WriteDescriptorSet::default()
 945                     .dst_set(descriptor_set)
 946                     .dst_binding(2)
 947                     .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
 948                     .buffer_info(&buffer_infos),
 949                 vk::WriteDescriptorSet::default()
 950                     .dst_set(descriptor_set)
 951                     .dst_binding(3)
 952                     .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
 953                     .buffer_info(&feature_infos),
 954                 // Snapshot twin: bindings 1-3 alias the same objects; binding 0
 955                 // is written by `sync_backdrop_targets` once the snapshot image
 956                 // exists.
 957                 vk::WriteDescriptorSet::default()
 958                     .dst_set(descriptor_set_snapshot)
 959                     .dst_binding(1)
 960                     .descriptor_type(vk::DescriptorType::SAMPLER)
 961                     .image_info(&sampler_infos),
 962                 vk::WriteDescriptorSet::default()
 963                     .dst_set(descriptor_set_snapshot)
 964                     .dst_binding(2)
 965                     .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
 966                     .buffer_info(&buffer_infos),
 967                 vk::WriteDescriptorSet::default()
 968                     .dst_set(descriptor_set_snapshot)
 969                     .dst_binding(3)
 970                     .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
 971                     .buffer_info(&feature_infos),
 972             ],
 973             &[],
 974         );
 975 
 976         // Per-frame command buffers, sync, and vertex buffers.
 977         let cmds = device
 978             .allocate_command_buffers(
 979                 &vk::CommandBufferAllocateInfo::default()
 980                     .command_pool(command_pool)
 981                     .level(vk::CommandBufferLevel::PRIMARY)
 982                     .command_buffer_count(FRAMES_IN_FLIGHT as u32),
 983             )
 984             .expect("Failed to allocate command buffers");
 985         let frames = cmds
 986             .into_iter()
 987             .map(|cmd| Frame {
 988                 cmd,
 989                 image_available: device
 990                     .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)
 991                     .unwrap(),
 992                 in_flight: device
 993                     .create_fence(
 994                         &vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED),
 995                         None,
 996                     )
 997                     .unwrap(),
 998                 vertex: create_cpu_buffer(
 999                     &device,
1000                     allocator,
1001                     64 * 1024,
1002                     vk::BufferUsageFlags::VERTEX_BUFFER,
1003                     "vertices",
1004                 ),
1005                 vertex_count: 0,
1006                 overlay_start: 0,
1007                 overlay_count: 0,
1008             })
1009             .collect();
1010 
1011         let text = TextStage::new(&device, allocator, render_pass, FRAMES_IN_FLIGHT);
1012         // Whether a mip chain can be built by blitting: both of the formats a
1013         // user image may be in have to be a blit's source and destination
1014         // and filter linearly. Asked here, where the instance is.
1015         let mips_supported = [vk::Format::R8G8B8A8_SRGB, vk::Format::B8G8R8A8_SRGB]
1016             .into_iter()
1017             .all(|format| {
1018                 let needed = vk::FormatFeatureFlags::BLIT_SRC
1019                     | vk::FormatFeatureFlags::BLIT_DST
1020                     | vk::FormatFeatureFlags::SAMPLED_IMAGE_FILTER_LINEAR;
1021                 unsafe {
1022                     core.instance
1023                         .get_physical_device_format_properties(core.physical_device, format)
1024                 }
1025                 .optimal_tiling_features
1026                 .contains(needed)
1027             });
1028         let blur_mips = {
1029             let needed = vk::FormatFeatureFlags::BLIT_SRC
1030                 | vk::FormatFeatureFlags::BLIT_DST
1031                 | vk::FormatFeatureFlags::SAMPLED_IMAGE_FILTER_LINEAR;
1032             unsafe {
1033                 core.instance
1034                     .get_physical_device_format_properties(core.physical_device, surface_format.format)
1035             }
1036             .optimal_tiling_features
1037             .contains(needed)
1038         };
1039         let image = ImageStage::new(
1040             &device,
1041             allocator,
1042             render_pass,
1043             FRAMES_IN_FLIGHT,
1044             mips_supported,
1045             core.max_anisotropy,
1046         );
1047 
1048         let swapchain_loader = ash::khr::swapchain::Device::new(&core.instance, &device);
1049         let mut renderer = Self {
1050             surface,
1051             swapchain_loader,
1052             swapchain: vk::SwapchainKHR::null(),
1053             swapchain_images: Vec::new(),
1054             surface_format,
1055             extent: vk::Extent2D { width: width.max(1), height: height.max(1) },
1056             swapchain_views: Vec::new(),
1057             framebuffers: Vec::new(),
1058             render_finished: Vec::new(),
1059             render_pass,
1060             render_pass_load,
1061             render_pass_partial,
1062             image_ages: Vec::new(),
1063             descriptor_set_layout,
1064             pipeline_layout,
1065             pipeline,
1066             shader_module,
1067             descriptor_pool,
1068             descriptor_set,
1069             descriptor_set_snapshot,
1070             snapshot_image: vk::Image::null(),
1071             snapshot_view: vk::ImageView::null(),
1072             snapshot_allocation: None,
1073             snapshot_wanted: false,
1074             snapshot_levels: 1,
1075             blur_mips,
1076             minimal_swapchain: false,
1077             backdrop_sampler,
1078             window_info,
1079             profile_gen: 0,
1080             relief_uploaded: (0.0, 0.0),
1081             roll_profile_gen: 0,
1082             plate_features,
1083             frames,
1084             frame_index: 0,
1085             text,
1086             scene,
1087             image,
1088             rt: None,
1089             rt_background: None,
1090             rt_environment: RtEnvironment::default(),
1091             desired_extent: vk::Extent2D { width: width.max(1), height: height.max(1) },
1092             corner_radius_px,
1093             swapchain_dirty: false,
1094             present_mode: vk::PresentModeKHR::FIFO,
1095             present_debug_count: 0,
1096             surface_lost: false,
1097             core,
1098         };
1099         log::debug!("[timing] VkRenderer pipelines/stages: {:?}", t_rest.elapsed());
1100         let t_swap = std::time::Instant::now();
1101         // On failure `renderer` drops here, and its Drop tears down everything
1102         // built so far, surface included.
1103         renderer.create_swapchain()?;
1104         renderer.write_window_info();
1105         // The swapchain may have settled on a different extent than requested;
1106         // keep the backdrop targets in lockstep.
1107         renderer.sync_backdrop_targets();
1108         log::debug!("[timing] swapchain setup: {:?}", t_swap.elapsed());
1109         Ok(renderer)
1110     }
1111 
1112     /// The window-clip corner radius as the shaders consume it: the nominal
1113     /// radius widened by the curvature-match factor, so the clip cuts along
1114     /// the same curve as window-scale plate corners (`plate_push_raised` with
1115     /// `scale_corners`) and a clipped window reads the same as a plate-drawn
1116     /// one. Capped at half the smaller extent, like the plate path's cap.
1117     fn clip_corner_radius(&self) -> f32 {
1118         let cap = 0.5 * self.extent.width.min(self.extent.height) as f32;
1119         (self.corner_radius_px * crate::layout::corner_span_factor()).min(cap)
1120     }
1121 
1122     /// The pinned relief heights in physical px, 0 = follow the width.
1123     fn relief_px(&self) -> (f32, f32) {
1124         relief_px_at(crate::scale::scale_factor())
1125     }
1126 
1127     fn write_window_info(&mut self) {
1128         // [size/clip vec4][carve profile meta vec4][8 vec4 carve slopes]
1129         // [roll profile meta vec4][8 vec4 roll slopes][relief heights vec4]
1130         // — must stay in lockstep with shader2d's WindowInfo. (The frost
1131         // recipe is per plate, in its push block, since RFC material step 3.)
1132         let relief = self.relief_px();
1133         let data = window_info_data(self.extent, self.clip_corner_radius(), relief);
1134         self.relief_uploaded = relief;
1135         // Every pixel shades differently now: no image may be patched.
1136         self.image_ages.fill(ImageAge::Unknown);
1137         self.profile_gen = crate::layout::bevel_profile_generation();
1138         self.roll_profile_gen = crate::layout::roll_profile_generation();
1139         if let Some(allocation) = self.window_info.allocation.as_mut() {
1140             allocation.mapped_slice_mut().unwrap()[..WINDOW_INFO_BYTES as usize]
1141                 .copy_from_slice(bytemuck::cast_slice(&data));
1142         }
1143     }
1144 
1145     fn destroy_swapchain_resources(&mut self) {
1146         unsafe {
1147             for fb in self.framebuffers.drain(..) {
1148                 self.core.device.destroy_framebuffer(fb, None);
1149             }
1150             for view in self.swapchain_views.drain(..) {
1151                 self.core.device.destroy_image_view(view, None);
1152             }
1153             self.swapchain_images.clear();
1154             for sem in self.render_finished.drain(..) {
1155                 self.core.device.destroy_semaphore(sem, None);
1156             }
1157         }
1158     }
1159 
1160     /// Build the swapchain for the current surface. Only the calls that ask
1161     /// the surface can fail with [`SurfaceLost`]; everything after them is
1162     /// device work and still panics as the bug it would be. A failure leaves
1163     /// the previous swapchain (if any) in `self.swapchain` for Drop.
1164     fn create_swapchain(&mut self) -> Result<(), SurfaceLost> {
1165         unsafe {
1166             let caps = self.core
1167                 .surface_loader
1168                 .get_physical_device_surface_capabilities(self.core.physical_device, self.surface)
1169                 .map_err(|result| SurfaceLost {
1170                     call: "vkGetPhysicalDeviceSurfaceCapabilitiesKHR",
1171                     result,
1172                 })?;
1173 
1174             // Wayland reports "extent defined by the swapchain" (u32::MAX); use the
1175             // size the configure events gave us.
1176             let extent = if caps.current_extent.width != u32::MAX {
1177                 caps.current_extent
1178             } else {
1179                 vk::Extent2D {
1180                     width: self
1181                         .desired_extent
1182                         .width
1183                         .clamp(caps.min_image_extent.width, caps.max_image_extent.width.max(1)),
1184                     height: self
1185                         .desired_extent
1186                         .height
1187                         .clamp(caps.min_image_extent.height, caps.max_image_extent.height.max(1)),
1188                 }
1189             };
1190 
1191             // One more than the minimum, so acquiring never waits on the
1192             // compositor to release one — except where the caller asked for
1193             // the minimum (`set_minimal_swapchain`).
1194             let mut image_count = caps.min_image_count + u32::from(!self.minimal_swapchain);
1195             if caps.max_image_count > 0 {
1196                 image_count = image_count.min(caps.max_image_count);
1197             }
1198 
1199             // Prefer premultiplied (what the DE's other clients pick), else opaque,
1200             // else whatever the surface offers.
1201             let composite_alpha = [
1202                 vk::CompositeAlphaFlagsKHR::PRE_MULTIPLIED,
1203                 vk::CompositeAlphaFlagsKHR::OPAQUE,
1204                 vk::CompositeAlphaFlagsKHR::POST_MULTIPLIED,
1205                 vk::CompositeAlphaFlagsKHR::INHERIT,
1206             ]
1207             .into_iter()
1208             .find(|&mode| caps.supported_composite_alpha.contains(mode))
1209             .unwrap_or(vk::CompositeAlphaFlagsKHR::OPAQUE);
1210 
1211             // MAILBOX when the driver offers it (Mesa Wayland always does):
1212             // FIFO's present throttle waits on the PREVIOUS present's frame
1213             // callback, and a surface the compositor never renders (off the
1214             // viewport) never gets one — the second-ever present then blocks
1215             // forever inside queue_present with the whole event loop behind
1216             // it. MAILBOX just replaces the queued buffer, so presenting to
1217             // an invisible surface is always safe. The demand-driven loop's
1218             // frame-callback gate keeps MAILBOX from free-running.
1219             let modes = self
1220                 .core
1221                 .surface_loader
1222                 .get_physical_device_surface_present_modes(self.core.physical_device, self.surface)
1223                 .unwrap_or_default();
1224             self.present_mode = if modes.contains(&vk::PresentModeKHR::MAILBOX) {
1225                 vk::PresentModeKHR::MAILBOX
1226             } else {
1227                 vk::PresentModeKHR::FIFO
1228             };
1229 
1230             let old_swapchain = self.swapchain;
1231             self.swapchain = self
1232                 .swapchain_loader
1233                 .create_swapchain(
1234                     &vk::SwapchainCreateInfoKHR::default()
1235                         .surface(self.surface)
1236                         .min_image_count(image_count)
1237                         .image_format(self.surface_format.format)
1238                         .image_color_space(self.surface_format.color_space)
1239                         .image_extent(extent)
1240                         .image_array_layers(1)
1241                         .image_usage(
1242                             vk::ImageUsageFlags::COLOR_ATTACHMENT
1243                                 | vk::ImageUsageFlags::TRANSFER_DST
1244                                 // Blur-behind plates copy the frame-so-far out
1245                                 // of the swapchain into the snapshot image.
1246                                 | vk::ImageUsageFlags::TRANSFER_SRC,
1247                         )
1248                         .image_sharing_mode(vk::SharingMode::EXCLUSIVE)
1249                         .pre_transform(caps.current_transform)
1250                         .composite_alpha(composite_alpha)
1251                         .present_mode(self.present_mode)
1252                         .clipped(true)
1253                         .old_swapchain(old_swapchain),
1254                     None,
1255                 )
1256                 .map_err(|result| SurfaceLost { call: "vkCreateSwapchainKHR", result })?;
1257             if old_swapchain != vk::SwapchainKHR::null() {
1258                 self.swapchain_loader.destroy_swapchain(old_swapchain, None);
1259             }
1260             self.extent = extent;
1261             if extent.width == self.desired_extent.width && extent.height == self.desired_extent.height {
1262                 // Keep the two in step so a rebuild queued for a non-resize
1263                 // reason (suboptimal/out-of-date) doesn't hand
1264                 // `pending_extent` a stale or unclamped size.
1265                 self.desired_extent = extent;
1266             } else {
1267                 // The surface capabilities overrode the requested size (seen
1268                 // on suspend/resume, when caps briefly lag the real surface
1269                 // state). Presenting this swapchain would commit a buffer the
1270                 // caller never approved — paired with the wrong buffer scale
1271                 // that reads as a self-resize and half/double-sizes the
1272                 // window. Keep the request, requeue the rebuild, and let
1273                 // draw_frame skip the present until caps agree.
1274                 log::warn!(
1275                     "swapchain extent {}x{} != requested {}x{}; skipping present until they agree",
1276                     extent.width, extent.height,
1277                     self.desired_extent.width, self.desired_extent.height,
1278                 );
1279                 self.swapchain_dirty = true;
1280             }
1281 
1282             let images = self
1283                 .swapchain_loader
1284                 .get_swapchain_images(self.swapchain)
1285                 .map_err(|result| SurfaceLost { call: "vkGetSwapchainImagesKHR", result })?;
1286             self.swapchain_images = images.clone();
1287             self.image_ages = vec![ImageAge::Unknown; images.len()];
1288             let subresource_range = vk::ImageSubresourceRange::default()
1289                 .aspect_mask(vk::ImageAspectFlags::COLOR)
1290                 .base_mip_level(0)
1291                 .level_count(1)
1292                 .base_array_layer(0)
1293                 .layer_count(1);
1294             for image in &images {
1295                 let view = self.core
1296                     .device
1297                     .create_image_view(
1298                         &vk::ImageViewCreateInfo::default()
1299                             .image(*image)
1300                             .view_type(vk::ImageViewType::TYPE_2D)
1301                             .format(self.surface_format.format)
1302                             .subresource_range(subresource_range),
1303                         None,
1304                     )
1305                     .expect("Failed to create swapchain view");
1306                 self.swapchain_views.push(view);
1307                 let attachments = [view];
1308                 let fb = self.core
1309                     .device
1310                     .create_framebuffer(
1311                         &vk::FramebufferCreateInfo::default()
1312                             .render_pass(self.render_pass)
1313                             .attachments(&attachments)
1314                             .width(extent.width)
1315                             .height(extent.height)
1316                             .layers(1),
1317                         None,
1318                     )
1319                     .expect("Failed to create framebuffer");
1320                 self.framebuffers.push(fb);
1321                 self.render_finished.push(
1322                     self.core.device
1323                         .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)
1324                         .unwrap(),
1325                 );
1326             }
1327         }
1328         Ok(())
1329     }
1330 
1331     fn recreate_swapchain(&mut self) -> Result<(), SurfaceLost> {
1332         unsafe {
1333             let _ = self.core.device.device_wait_idle();
1334         }
1335         let before = self.extent;
1336         self.destroy_swapchain_resources();
1337         self.create_swapchain()?;
1338         if present_debug() {
1339             eprintln!(
1340                 "[vk] swapchain rebuilt {}x{} -> {}x{} (backdrop valid: {})",
1341                 before.width, before.height, self.extent.width, self.extent.height,
1342                 self.scene.backdrop_valid,
1343             );
1344         }
1345         self.write_window_info();
1346         self.sync_backdrop_targets();
1347         Ok(())
1348     }
1349 
1350     /// Latch a lost surface: say so once, then skip draws until a new
1351     /// surface is attached.
1352     fn mark_surface_lost(&mut self, lost: SurfaceLost) {
1353         if !self.surface_lost {
1354             log::warn!("[vk] {lost}; skipping draws until the connection is replaced");
1355         }
1356         self.surface_lost = true;
1357     }
1358 
1359     /// Whether the surface has been reported lost (see [`SurfaceLost`]). A
1360     /// caller with its own event loop can end its session on this rather
1361     /// than wait for the connection error.
1362     pub fn surface_lost(&self) -> bool {
1363         self.surface_lost
1364     }
1365 
1366     /// Suspend the UI pass, copy the swapchain's frame-so-far into the blur
1367     /// snapshot image, and resume drawing — the mechanism behind blur-behind
1368     /// plates (`Batch2D::blur_behind`). Ending the pass leaves the swapchain in
1369     /// its PRESENT final layout; the copy walks it through TRANSFER_SRC and
1370     /// hands it back in TRANSFER_DST, which is exactly `render_pass_load`'s
1371     /// expected initial layout, so the resume reuses that pass (and the shared
1372     /// framebuffers). Dynamic viewport state dies with the pass and is restored;
1373     /// scissor/pipeline/descriptors are re-bound per draw by the batch loop.
1374     fn snapshot_frame_so_far(&self, cmd: vk::CommandBuffer, image_index: usize) {
1375         let device = &self.core.device;
1376         let swapchain_image = self.swapchain_images[image_index];
1377         unsafe {
1378             device.cmd_end_render_pass(cmd);
1379             device.cmd_pipeline_barrier(
1380                 cmd,
1381                 vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
1382                     | vk::PipelineStageFlags::FRAGMENT_SHADER,
1383                 vk::PipelineStageFlags::TRANSFER,
1384                 vk::DependencyFlags::empty(),
1385                 &[],
1386                 &[],
1387                 &[
1388                     vk::ImageMemoryBarrier::default()
1389                         .src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
1390                         .dst_access_mask(vk::AccessFlags::TRANSFER_READ)
1391                         .old_layout(vk::ImageLayout::PRESENT_SRC_KHR)
1392                         .new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
1393                         .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1394                         .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1395                         .image(swapchain_image)
1396                         .subresource_range(COLOR_RANGE),
1397                     // Covers the previous frame's blur reads of the snapshot,
1398                     // every level of it: the whole chain is rewritten.
1399                     vk::ImageMemoryBarrier::default()
1400                         .src_access_mask(vk::AccessFlags::SHADER_READ)
1401                         .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
1402                         .old_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
1403                         .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
1404                         .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1405                         .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1406                         .image(self.snapshot_image)
1407                         .subresource_range(color_levels(0, self.snapshot_levels)),
1408                 ],
1409             );
1410             let subresource = vk::ImageSubresourceLayers::default()
1411                 .aspect_mask(vk::ImageAspectFlags::COLOR)
1412                 .layer_count(1);
1413             device.cmd_copy_image(
1414                 cmd,
1415                 swapchain_image,
1416                 vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
1417                 self.snapshot_image,
1418                 vk::ImageLayout::TRANSFER_DST_OPTIMAL,
1419                 &[vk::ImageCopy::default()
1420                     .src_subresource(subresource)
1421                     .dst_subresource(subresource)
1422                     .extent(vk::Extent3D {
1423                         width: self.extent.width,
1424                         height: self.extent.height,
1425                         depth: 1,
1426                     })],
1427             );
1428             self.snapshot_mip_chain(cmd);
1429             device.cmd_pipeline_barrier(
1430                 cmd,
1431                 vk::PipelineStageFlags::TRANSFER,
1432                 vk::PipelineStageFlags::FRAGMENT_SHADER | vk::PipelineStageFlags::TRANSFER,
1433                 vk::DependencyFlags::empty(),
1434                 &[],
1435                 &[],
1436                 &[
1437                     vk::ImageMemoryBarrier::default()
1438                         .src_access_mask(vk::AccessFlags::TRANSFER_READ)
1439                         .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
1440                         .old_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
1441                         .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
1442                         .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1443                         .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1444                         .image(swapchain_image)
1445                         .subresource_range(COLOR_RANGE),
1446                 ],
1447             );
1448             device.cmd_begin_render_pass(
1449                 cmd,
1450                 &vk::RenderPassBeginInfo::default()
1451                     .render_pass(self.render_pass_load)
1452                     .framebuffer(self.framebuffers[image_index])
1453                     .render_area(vk::Rect2D {
1454                         offset: vk::Offset2D { x: 0, y: 0 },
1455                         extent: self.extent,
1456                     }),
1457                 vk::SubpassContents::INLINE,
1458             );
1459             device.cmd_set_viewport(cmd, 0, &[flipped_viewport(self.extent)]);
1460         }
1461     }
1462 
1463     /// Build the snapshot's mip chain from the level 0 just copied in, and
1464     /// leave every level SHADER_READ_ONLY. Expects every level in
1465     /// TRANSFER_DST, as `snapshot_frame_so_far` leaves them.
1466     ///
1467     /// The chain is what the frost kernel samples (shader2d's `resolve_blur`):
1468     /// its 7x7 taps stand a whole STRIDE apart — 5.5 physical px for the
1469     /// panel's default kernel — and a tap at level 0 reads only the texel or
1470     /// two it lands between. So anything behind a plate thinner than the
1471     /// stride (a hairline, a well's edge, a glyph) was not blurred but picked
1472     /// up whole by the taps that hit it and missed by the rest: seven faint
1473     /// copies a stride apart, which over a UI's rows read as horizontal
1474     /// bands. Read at the level whose texel is as wide as the stride, each tap
1475     /// is already the average of the cell around it, and the copies merge
1476     /// into one smooth smear. Each level is a linear-filtered blit of the
1477     /// one above it, a 2x2 box.
1478     fn snapshot_mip_chain(&self, cmd: vk::CommandBuffer) {
1479         let device = &self.core.device;
1480         let barrier = |level: u32, src: vk::AccessFlags, dst: vk::AccessFlags, old: vk::ImageLayout, new: vk::ImageLayout| {
1481             vk::ImageMemoryBarrier::default()
1482                 .src_access_mask(src)
1483                 .dst_access_mask(dst)
1484                 .old_layout(old)
1485                 .new_layout(new)
1486                 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1487                 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1488                 .image(self.snapshot_image)
1489                 .subresource_range(color_levels(level, 1))
1490         };
1491         let size = |level: u32| {
1492             [
1493                 (self.extent.width >> level).max(1) as i32,
1494                 (self.extent.height >> level).max(1) as i32,
1495             ]
1496         };
1497         unsafe {
1498             for level in 1..self.snapshot_levels {
1499                 // The level above is written; read it for the blit.
1500                 device.cmd_pipeline_barrier(
1501                     cmd,
1502                     vk::PipelineStageFlags::TRANSFER,
1503                     vk::PipelineStageFlags::TRANSFER,
1504                     vk::DependencyFlags::empty(),
1505                     &[],
1506                     &[],
1507                     &[barrier(
1508                         level - 1,
1509                         vk::AccessFlags::TRANSFER_WRITE,
1510                         vk::AccessFlags::TRANSFER_READ,
1511                         vk::ImageLayout::TRANSFER_DST_OPTIMAL,
1512                         vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
1513                     )],
1514                 );
1515                 let ([sw, sh], [dw, dh]) = (size(level - 1), size(level));
1516                 let layers = |mip: u32| {
1517                     vk::ImageSubresourceLayers::default()
1518                         .aspect_mask(vk::ImageAspectFlags::COLOR)
1519                         .mip_level(mip)
1520                         .layer_count(1)
1521                 };
1522                 device.cmd_blit_image(
1523                     cmd,
1524                     self.snapshot_image,
1525                     vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
1526                     self.snapshot_image,
1527                     vk::ImageLayout::TRANSFER_DST_OPTIMAL,
1528                     &[vk::ImageBlit::default()
1529                         .src_subresource(layers(level - 1))
1530                         .src_offsets([vk::Offset3D::default(), vk::Offset3D { x: sw, y: sh, z: 1 }])
1531                         .dst_subresource(layers(level))
1532                         .dst_offsets([vk::Offset3D::default(), vk::Offset3D { x: dw, y: dh, z: 1 }])],
1533                     vk::Filter::LINEAR,
1534                 );
1535             }
1536             // Every level sampleable: the ones blitted FROM are in
1537             // TRANSFER_SRC, the last (or the only) one still in TRANSFER_DST.
1538             let last = self.snapshot_levels - 1;
1539             let mut to_read: Vec<vk::ImageMemoryBarrier> = (0..last)
1540                 .map(|l| {
1541                     barrier(
1542                         l,
1543                         vk::AccessFlags::TRANSFER_READ,
1544                         vk::AccessFlags::SHADER_READ,
1545                         vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
1546                         vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
1547                     )
1548                 })
1549                 .collect();
1550             to_read.push(barrier(
1551                 last,
1552                 vk::AccessFlags::TRANSFER_WRITE,
1553                 vk::AccessFlags::SHADER_READ,
1554                 vk::ImageLayout::TRANSFER_DST_OPTIMAL,
1555                 vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
1556             ));
1557             device.cmd_pipeline_barrier(
1558                 cmd,
1559                 vk::PipelineStageFlags::TRANSFER,
1560                 vk::PipelineStageFlags::FRAGMENT_SHADER,
1561                 vk::DependencyFlags::empty(),
1562                 &[],
1563                 &[],
1564                 &to_read,
1565             );
1566         }
1567     }
1568 
1569     /// Recreate backdrop + depth at the surface size (device must be idle),
1570     /// re-point the UI descriptor at the new view, and make the fresh image
1571     /// legal to sample.
1572     ///
1573     /// A rebuild at the SAME size (a swapchain reported suboptimal or out of
1574     /// date, a corner-radius change) keeps the backdrop, and must not clear
1575     /// it: `backdrop_valid` stays true across it, so a cleared image is
1576     /// replayed under the UI as the scene, and an app that stages only when
1577     /// its scene changes never repairs it. Seen as a designer viewport that
1578     /// stayed black until the pointer moved, on a discrete GPU presenting to
1579     /// a compositor on the integrated one — its swapchain is rebuilt at the
1580     /// same size a few frames in (`CCE_PRESENT_DEBUG` logs every rebuild).
1581     fn sync_backdrop_targets(&mut self) {
1582         let extent = self.scene.target_extent(self.extent);
1583         let recreated = self.scene.resize(
1584             &self.core.device,
1585             self.core.allocator.as_mut().unwrap(),
1586             extent,
1587         );
1588         if recreated {
1589             clear_image_to_shader_read(
1590                 &self.core.device,
1591                 self.core.queue,
1592                 self.core.command_pool,
1593                 self.scene.backdrop_image,
1594             );
1595         }
1596         let image_infos = [vk::DescriptorImageInfo::default()
1597             .image_view(self.scene.backdrop_view)
1598             .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)];
1599         unsafe {
1600             self.core.device.update_descriptor_sets(
1601                 &[vk::WriteDescriptorSet::default()
1602                     .dst_set(self.descriptor_set)
1603                     .dst_binding(0)
1604                     .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
1605                     .image_info(&image_infos)],
1606                 &[],
1607             );
1608         }
1609         if self.snapshot_wanted {
1610             self.sync_snapshot_target();
1611         }
1612     }
1613 
1614     /// (Re)create the blur snapshot at the surface size and point its
1615     /// descriptor set at it. Only for a renderer that has drawn a blur plate
1616     /// needing one (`snapshot_wanted`): most windows frost only their root
1617     /// plate, which reads the zeroed backdrop instead (`first_frost_exempt`),
1618     /// and the grid draws no frost at all — and the snapshot is a whole
1619     /// surface, ~16 MiB for a 2560x1600 window and ~240 MiB for the grid's
1620     /// patch. The device must not be using the snapshot set (idle, or the set
1621     /// never bound because the snapshot never existed).
1622     fn sync_snapshot_target(&mut self) {
1623         let levels = if self.blur_mips { snapshot_levels(self.extent) } else { 1 };
1624         self.snapshot_levels = levels;
1625         // Same format as the swapchain, so cmd_copy_image from it is legal.
1626         unsafe {
1627             let device = &self.core.device;
1628             if self.snapshot_view != vk::ImageView::null() {
1629                 device.destroy_image_view(self.snapshot_view, None);
1630                 device.destroy_image(self.snapshot_image, None);
1631                 self.snapshot_view = vk::ImageView::null();
1632                 self.snapshot_image = vk::Image::null();
1633             }
1634             if let Some(alloc) = self.snapshot_allocation.take() {
1635                 let _ = self.core.allocator.as_mut().unwrap().free(alloc);
1636             }
1637             let device = &self.core.device;
1638             let snapshot_image = device
1639                 .create_image(
1640                     &vk::ImageCreateInfo::default()
1641                         .image_type(vk::ImageType::TYPE_2D)
1642                         .format(self.surface_format.format)
1643                         .extent(vk::Extent3D {
1644                             width: self.extent.width,
1645                             height: self.extent.height,
1646                             depth: 1,
1647                         })
1648                         .mip_levels(levels)
1649                         .array_layers(1)
1650                         .samples(vk::SampleCountFlags::TYPE_1)
1651                         .tiling(vk::ImageTiling::OPTIMAL)
1652                         .usage(
1653                             vk::ImageUsageFlags::SAMPLED
1654                                 | vk::ImageUsageFlags::TRANSFER_DST
1655                                 | vk::ImageUsageFlags::TRANSFER_SRC,
1656                         )
1657                         .initial_layout(vk::ImageLayout::UNDEFINED),
1658                     None,
1659                 )
1660                 .expect("Failed to create snapshot image");
1661             let requirements = device.get_image_memory_requirements(snapshot_image);
1662             let allocation = self
1663                 .core
1664                 .allocator
1665                 .as_mut()
1666                 .unwrap()
1667                 .allocate(&AllocationCreateDesc {
1668                     name: "blur-snapshot",
1669                     requirements,
1670                     location: MemoryLocation::GpuOnly,
1671                     linear: false,
1672                     allocation_scheme: AllocationScheme::GpuAllocatorManaged,
1673                 })
1674                 .expect("Failed to allocate snapshot memory");
1675             self.core
1676                 .device
1677                 .bind_image_memory(snapshot_image, allocation.memory(), allocation.offset())
1678                 .expect("Failed to bind snapshot memory");
1679             let snapshot_view = self
1680                 .core
1681                 .device
1682                 .create_image_view(
1683                     &vk::ImageViewCreateInfo::default()
1684                         .image(snapshot_image)
1685                         .view_type(vk::ImageViewType::TYPE_2D)
1686                         .format(self.surface_format.format)
1687                         .subresource_range(color_levels(0, levels)),
1688                     None,
1689                 )
1690                 .expect("Failed to create snapshot view");
1691             self.snapshot_image = snapshot_image;
1692             self.snapshot_view = snapshot_view;
1693             self.snapshot_allocation = Some(allocation);
1694         }
1695         // A fresh snapshot must be legal to sample before its first copy.
1696         clear_image_levels_to_shader_read(
1697             &self.core.device,
1698             self.core.queue,
1699             self.core.command_pool,
1700             self.snapshot_image,
1701             levels,
1702         );
1703         let snapshot_infos = [vk::DescriptorImageInfo::default()
1704             .image_view(self.snapshot_view)
1705             .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)];
1706         unsafe {
1707             self.core.device.update_descriptor_sets(
1708                 &[vk::WriteDescriptorSet::default()
1709                     .dst_set(self.descriptor_set_snapshot)
1710                     .dst_binding(0)
1711                     .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
1712                     .image_info(&snapshot_infos)],
1713                 &[],
1714             );
1715         }
1716     }
1717 
1718     /// Upload a 3D mesh (Vertex3D: position + color); the id is stable for the
1719     /// renderer's lifetime.
1720     pub fn create_mesh(&mut self, verts: &[Vertex3D]) -> MeshId {
1721         self.scene
1722             .create_mesh(&self.core.device, self.core.allocator.as_mut().unwrap(), verts)
1723     }
1724 
1725     /// Replace a mesh's vertices, without waiting for the GPU: the frames
1726     /// in flight keep the buffer they read, and the new vertices go into
1727     /// another (`SceneStage::update_mesh`). It waited for the device to go
1728     /// idle until 2026-10-07, which every frame of a playing simulation
1729     /// paid.
1730     pub fn update_mesh(&mut self, id: MeshId, verts: &[Vertex3D]) {
1731         self.scene
1732             .update_mesh(&self.core.device, self.core.allocator.as_mut().unwrap(), id, verts);
1733     }
1734 
1735     /// Stage the 3D scene for the next `draw_frame`. Draws render into the
1736     /// backdrop image (scissored to the viewport pane, physical pixels), which
1737     /// is copied beneath the UI and doubles as the blur-behind source. Frames
1738     /// with no staged scene reuse the previous backdrop — the ash equivalent of
1739     /// the app's viewport-changed cache.
1740     pub fn stage_scene(&mut self, scissor: (u32, u32, u32, u32), draws: Vec<SceneDraw>) {
1741         self.want_scene_targets();
1742         self.scene.stage(scissor, draws);
1743     }
1744 
1745     /// Use the surface's minimum swapchain image count instead of one more.
1746     /// For a surface that redraws rarely and is large — the desktop grid's
1747     /// patch is ~240 MiB an image on a HiDPI panel — where the spare image
1748     /// costs more than an occasional wait on the compositor. Takes effect at
1749     /// the next swapchain rebuild.
1750     pub fn set_minimal_swapchain(&mut self) {
1751         if !self.minimal_swapchain {
1752             self.minimal_swapchain = true;
1753             self.swapchain_dirty = true;
1754         }
1755     }
1756 
1757     /// Grow the backdrop and depth targets to the surface the first time a
1758     /// scene is staged; until then they are 1×1 (`Scene::target_extent`).
1759     /// Runs between frames, the device idle, as a resize does.
1760     fn want_scene_targets(&mut self) {
1761         if self.scene.wanted {
1762             return;
1763         }
1764         self.scene.wanted = true;
1765         if self.surface == vk::SurfaceKHR::null() {
1766             return; // sized with the swapchain when one exists
1767         }
1768         unsafe {
1769             let _ = self.core.device.device_wait_idle();
1770         }
1771         self.sync_backdrop_targets();
1772     }
1773 
1774     /// Add textured quads to the scene staged by the last [`stage_scene`] —
1775     /// user images (ids from `upload_rgba`) standing in the 3D world, depth
1776     /// tested against the meshes. Call it AFTER `stage_scene`, which starts
1777     /// every staged scene with none; with no scene staged it does nothing.
1778     ///
1779     /// [`stage_scene`]: Self::stage_scene
1780     pub fn stage_scene_images(&mut self, images: Vec<SceneImage>) {
1781         self.scene.stage_images(images);
1782     }
1783 
1784     /// Upload a lit mesh (`draw::lit`). The first one also uploads the 1x1
1785     /// white image an untextured lit draw binds.
1786     pub fn create_lit_mesh(&mut self, verts: &[crate::draw::lit::LitVertex]) -> crate::draw::lit::LitMeshId {
1787         if self.scene.lit_fallback_image.is_none() {
1788             self.scene.lit_fallback_image = Some(self.upload_rgba_now(&[255, 255, 255, 255], 1, 1));
1789         }
1790         self.scene.create_lit_mesh(&self.core.device, self.core.allocator.as_mut().unwrap(), verts)
1791     }
1792 
1793     /// Replace a lit mesh's vertices; waits for the GPU first, as `update_mesh`.
1794     pub fn update_lit_mesh(&mut self, id: crate::draw::lit::LitMeshId, verts: &[crate::draw::lit::LitVertex]) {
1795         // No wait for the device: as `update_mesh`.
1796         self.scene.update_lit_mesh(&self.core.device, self.core.allocator.as_mut().unwrap(), id, verts);
1797     }
1798 
1799     /// The light lit draws are shaded by.
1800     pub fn set_lit_light(&mut self, light: crate::draw::lit::LitLight) {
1801         self.scene.lit_light = light;
1802     }
1803 
1804     /// This frame's lit draws, after `stage_scene`.
1805     pub fn stage_lit(&mut self, draws: Vec<crate::draw::lit::LitDraw>) {
1806         self.scene.stage_lit(draws);
1807     }
1808 
1809     /// Replace the path tracer's scene (triangles in the space the camera's
1810     /// `inv_mvp` unprojects into). Builds the BVH on the CPU and uploads it;
1811     /// waits for the GPU to go idle first — scene replacement is rare
1812     /// (geometry rebuilds), matching `update_mesh`. The first call compiles
1813     /// the compute pipeline. A large scene freezes the caller for the BVH
1814     /// build: build a [`PreparedRtScene`] on a worker instead and hand it to
1815     /// [`set_rt_scene_prepared`](Self::set_rt_scene_prepared).
1816     pub fn set_rt_scene(&mut self, triangles: &[RtTriangle], materials: &[RtMaterial]) {
1817         self.set_rt_scene_with_image(triangles, materials, None);
1818     }
1819 
1820     /// [`set_rt_scene`](Self::set_rt_scene), with a user image standing in
1821     /// the scene: the picture the raster pass draws as a `SceneImage`,
1822     /// traced. The image's pixels changing is a change of scene like any
1823     /// other — set it again, which restarts the accumulation.
1824     pub fn set_rt_scene_with_image(
1825         &mut self,
1826         triangles: &[RtTriangle],
1827         materials: &[RtMaterial],
1828         image: Option<RtImage>,
1829     ) {
1830         let prepared = PreparedRtScene::new(triangles.to_vec(), materials, image, self.rt_needs_bvh());
1831         self.set_rt_scene_prepared(&prepared);
1832     }
1833 
1834     /// Replace the path tracer's scene with one prepared off this thread
1835     /// ([`PreparedRtScene::new`]): only the upload — buffers written, and
1836     /// on the ray-query tier the acceleration structures built on the GPU.
1837     /// Waits for the GPU to go idle first, as `set_rt_scene` does. A scene
1838     /// prepared without a BVH gets one built here if this renderer needs it.
1839     pub fn set_rt_scene_prepared(&mut self, scene: &PreparedRtScene) {
1840         unsafe {
1841             let _ = self.core.device.device_wait_idle();
1842         }
1843         let core = &mut self.core;
1844         let allocator = core.allocator.as_mut().unwrap();
1845         let rt = self.rt.get_or_insert_with(|| {
1846             RtStage::new(
1847                 &core.device,
1848                 allocator,
1849                 FRAMES_IN_FLIGHT,
1850                 core.accel_loader.as_ref(),
1851                 core.as_scratch_align,
1852                 core.min_uniform_align,
1853                 core.queue,
1854                 core.command_pool,
1855             )
1856         });
1857         rt.set_scene(
1858             &core.device,
1859             allocator,
1860             core.queue,
1861             core.command_pool,
1862             &scene.packed,
1863             scene.image.map(|i| (RtImageSource::Shared(i.image), i.corners, i.opacity)),
1864         );
1865     }
1866 
1867     /// Whether this renderer's tracer traverses a CPU-built BVH (the compute
1868     /// tier) rather than building driver acceleration structures (the
1869     /// hardware ray-query tier): the `with_bvh` a [`PreparedRtScene`] for it
1870     /// wants. Answered before the first scene from the device's features.
1871     pub fn rt_needs_bvh(&self) -> bool {
1872         match &self.rt {
1873             Some(rt) => rt.needs_bvh(),
1874             None => crate::vk::rt::needs_bvh(self.core.accel_loader.is_some()),
1875         }
1876     }
1877 
1878     /// The direction TOWARD the 3D pass's light, in world space (any
1879     /// length; zero is ignored). The flat shading reads it, and a host that
1880     /// bakes smooth shading (`SceneDraw::prelit`) should light by the same
1881     /// one. Until a host sets it the light is the one this pass always had.
1882     /// A traced pane has its own, in [`RtEnvironment`]; a host that wants
1883     /// the two views to agree hands both the same direction.
1884     pub fn set_scene_light(&mut self, toward: [f32; 3]) {
1885         let v = glam::Vec3::from_array(toward);
1886         if v.length_squared() > 1e-12 {
1887             self.scene.light = v.normalize().to_array();
1888         }
1889     }
1890 
1891     /// The traced pane's sky and sun — see [`RtEnvironment`]. Kept here and
1892     /// handed over at each `stage_rt`, like the background; a change
1893     /// restarts the accumulation.
1894     pub fn set_rt_environment(&mut self, environment: RtEnvironment) {
1895         self.rt_environment = environment;
1896     }
1897 
1898     /// What a camera ray that meets nothing shows in the traced pane: a
1899     /// colour in LINEAR RGB (what the raster pass's vertex colours are), or
1900     /// None for the sky, which is what every miss showed until 2026-10-02.
1901     /// Only the camera ray: a bounce that leaves the scene still meets the
1902     /// sky, the tracer's one light, so a backdrop changes what is seen
1903     /// behind the scene and not how it is lit. Kept here and handed over at
1904     /// each `stage_rt`, so it may be set before the first scene; a change
1905     /// restarts the accumulation.
1906     pub fn set_rt_background(&mut self, color: Option<[f32; 3]>) {
1907         self.rt_background = color;
1908     }
1909 
1910     /// Stage one progressive path-tracing pass into the viewport pane
1911     /// (physical pixels) for the next `draw_frame`. Call it every frame while
1912     /// RT mode is on: each frame adds a sample; a camera/pane/scene change
1913     /// restarts the accumulation. No-op until `set_rt_scene` has run.
1914     pub fn stage_rt(&mut self, pane: (u32, u32, u32, u32), camera: RtCamera) {
1915         // The tracer blits into the backdrop at the surface's size.
1916         self.want_scene_targets();
1917         if let Some(rt) = self.rt.as_mut() {
1918             rt.set_background(self.rt_background);
1919             rt.set_environment(self.rt_environment);
1920             rt.stage(
1921                 &self.core.device,
1922                 self.core.allocator.as_mut().unwrap(),
1923                 pane,
1924                 camera,
1925             );
1926         }
1927     }
1928 
1929     /// True while more `stage_rt` + `draw_frame` rounds would still refine the
1930     /// image — the app's cue to keep requesting frames.
1931     pub fn rt_accumulating(&self) -> bool {
1932         self.rt.as_ref().is_some_and(|rt| rt.accumulating())
1933     }
1934 
1935     /// Whether presenting past an unacknowledged frame callback is safe.
1936     /// True under MAILBOX (the present replaces the queued buffer). Under
1937     /// FIFO the driver's present throttle waits on the previous present's
1938     /// frame event, so a forced present to a surface the compositor isn't
1939     /// rendering blocks forever — the caller must not force one.
1940     pub fn forced_present_safe(&self) -> bool {
1941         self.present_mode == vk::PresentModeKHR::MAILBOX
1942     }
1943 
1944     /// Let go of the window surface: wait idle, then destroy the swapchain
1945     /// and the `VkSurfaceKHR`, keeping the device, pipelines and atlases. The
1946     /// `wl_surface` under them may be destroyed after this returns, and must
1947     /// not be before — a swapchain presenting to a dead surface is undefined.
1948     /// Until [`attach_surface`](Self::attach_surface), `draw_frame_2d` draws
1949     /// nothing and returns false.
1950     pub fn detach_surface(&mut self) {
1951         unsafe {
1952             let _ = self.core.device.device_wait_idle();
1953             self.destroy_swapchain_resources();
1954             if self.swapchain != vk::SwapchainKHR::null() {
1955                 self.swapchain_loader.destroy_swapchain(self.swapchain, None);
1956                 self.swapchain = vk::SwapchainKHR::null();
1957             }
1958             if self.surface != vk::SurfaceKHR::null() {
1959                 self.core.surface_loader.destroy_surface(self.surface, None);
1960                 self.surface = vk::SurfaceKHR::null();
1961             }
1962         }
1963         self.extent = vk::Extent2D { width: 0, height: 0 };
1964         self.swapchain_dirty = true;
1965     }
1966 
1967     /// Present to a different `wl_surface` from now on, at `width` x
1968     /// `height` physical px — detaching from the current one first if it is
1969     /// still attached. What makes a popup surface cheap to re-open: a new
1970     /// renderer costs a device and every pipeline, this costs one swapchain.
1971     ///
1972     /// # Safety
1973     /// Same contract as [`VkRenderer::try_new`]: live `wl_display` / `wl_surface`
1974     /// pointers that outlive the attachment.
1975     pub unsafe fn attach_surface(
1976         &mut self,
1977         display_ptr: *mut c_void,
1978         surface_ptr: *mut c_void,
1979         width: u32,
1980         height: u32,
1981     ) -> Result<(), SurfaceLost> {
1982         self.attach_surface_to(
1983             super::core::SurfaceTarget::Wayland { display: display_ptr, surface: surface_ptr },
1984             width,
1985             height,
1986         )
1987     }
1988 
1989     /// [`attach_surface`](Self::attach_surface) for any window
1990     /// [`SurfaceTarget`](super::core::SurfaceTarget).
1991     ///
1992     /// # Safety
1993     /// The target's pointers must be live and outlive the attachment.
1994     pub unsafe fn attach_surface_to(
1995         &mut self,
1996         target: super::core::SurfaceTarget,
1997         width: u32,
1998         height: u32,
1999     ) -> Result<(), SurfaceLost> {
2000         if self.surface != vk::SurfaceKHR::null() {
2001             self.detach_surface();
2002         }
2003         self.surface = self.core.create_surface(target)?;
2004         self.surface_lost = false;
2005         self.resize(width, height);
2006         self.swapchain_dirty = true;
2007         Ok(())
2008     }
2009 
2010     /// Whether a surface is attached — false between
2011     /// [`detach_surface`](Self::detach_surface) and the next attach.
2012     pub fn has_surface(&self) -> bool {
2013         self.surface != vk::SurfaceKHR::null()
2014     }
2015 
2016     /// Whether this renderer takes the process-wide image upload queue
2017     /// ([`crate::vk::upload_rgba`] and kin) — true by default. A renderer
2018     /// that keeps images of its own, uploaded with
2019     /// [`upload_rgba_now`](Self::upload_rgba_now), turns it off so it never
2020     /// takes an upload the window's renderer was meant to draw.
2021     pub fn set_shared_uploads(&mut self, on: bool) {
2022         self.image.shared_uploads = on;
2023     }
2024 
2025     /// Upload RGBA8 pixels into this renderer's image table now, returning
2026     /// the id to draw them by in this renderer's frames.
2027     pub fn upload_rgba_now(&mut self, pixels: &[u8], width: u32, height: u32) -> u32 {
2028         self.image.upload_now(
2029             &self.core.device,
2030             self.core.allocator.as_mut().unwrap(),
2031             self.core.queue,
2032             self.core.command_pool,
2033             pixels,
2034             width,
2035             height,
2036         )
2037     }
2038 
2039     /// The extent the next `draw_frame` will render at: the pending size when a
2040     /// swapchain rebuild is queued, otherwise the live one.
2041     pub fn pending_extent(&self) -> vk::Extent2D {
2042         if self.swapchain_dirty { self.desired_extent } else { self.extent }
2043     }
2044 
2045     /// Request a new physical size (from xdg configure / scale changes). Applied
2046     /// lazily on the next `draw_frame`.
2047     pub fn resize(&mut self, width: u32, height: u32) {
2048         let extent = vk::Extent2D { width: width.max(1), height: height.max(1) };
2049         // Record the request unconditionally, not just when it differs from the
2050         // live extent: with a rebuild already queued (`swapchain_dirty`), a
2051         // request that returns to the live size must overwrite the queued one.
2052         // Otherwise `desired_extent` stays wedged at the intermediate size, the
2053         // caller's `pending_extent` gate never matches, and no frame presents
2054         // again — a resume scale bounce (2→1→2 before any draw) froze the
2055         // status-bar clock exactly this way.
2056         self.desired_extent = extent;
2057         if extent.width != self.extent.width || extent.height != self.extent.height {
2058             self.swapchain_dirty = true;
2059         }
2060     }
2061 
2062     // Used at cutover, when scale changes re-derive the radius; vk-smoke fixes it at init.
2063     // Nominal (circle-equivalent) radius in physical px — the curvature-match
2064     // widening for squircle corner shapes happens at consumption
2065     // (`clip_corner_radius`), so callers pass the configured radius as-is.
2066     #[allow(dead_code)]
2067     pub fn set_corner_radius(&mut self, radius_px: f32) {
2068         self.corner_radius_px = radius_px;
2069         // Written on the next swapchain rebuild or draw-idle moment; a mapped write
2070         // here would race in-flight frames, so route it through the dirty path.
2071         self.swapchain_dirty = true;
2072     }
2073 
2074     /// Stage text for the next `draw_frame`: shape-cache misses are rasterized
2075     /// into the glyph atlas and vertices are built against the current extent.
2076     /// Mirrors what was `glyphon::TextRenderer::prepare`.
2077     pub fn prepare_text(
2078         &mut self,
2079         font_system: &mut cosmic_text::FontSystem,
2080         swash_cache: &mut cosmic_text::SwashCache,
2081         spans: &[TextSpan<'_>],
2082     ) {
2083         // Against the extent this frame will actually be drawn at: `resize` is
2084         // lazy, so with a rebuild pending `self.extent` is still the previous
2085         // size and text would land in the wrong NDC (visibly mis-scaled and
2086         // offset while a window auto-sizes to its content).
2087         self.text.prepare(font_system, swash_cache, spans, self.pending_extent());
2088     }
2089 
2090     /// Render one frame of plain 2D geometry: a single unclipped batch, no
2091     /// overlay, transparent clear. See [`VkRenderer::draw_frame_2d`].
2092     pub fn draw_frame(&mut self, verts: &[Vertex]) -> bool {
2093         self.draw_frame_2d(Frame2D {
2094             verts,
2095             batches: &[],
2096             overlay_verts: &[],
2097             images: &[],
2098             plate_features: &[],
2099             clear_color: [0.0; 4],
2100             damage: None,
2101         })
2102     }
2103 
2104     /// Render one frame: the 2D geometry (optionally as scissored batches),
2105     /// then any text staged via `prepare_text`, then the overlay vertices on
2106     /// top. Returns false if the frame was skipped (swapchain rebuild); the
2107     /// caller just draws again next tick.
2108     pub fn draw_frame_2d(&mut self, frame2d: Frame2D<'_>) -> bool {
2109         if self.surface == vk::SurfaceKHR::null() || self.surface_lost {
2110             return false;
2111         }
2112         if self.swapchain_dirty {
2113             self.swapchain_dirty = false;
2114             if let Err(lost) = self.recreate_swapchain() {
2115                 self.mark_surface_lost(lost);
2116                 return false;
2117             }
2118             if self.swapchain_dirty {
2119                 // The rebuild couldn't honor the requested extent (surface
2120                 // caps disagree, e.g. mid suspend/resume) — presenting it
2121                 // would commit a wrong-size buffer. Skip; the caller redraws.
2122                 return false;
2123             }
2124         }
2125 
2126         unsafe {
2127             let frame_index = self.frame_index;
2128             let (in_flight, image_available) = {
2129                 let f = &self.frames[frame_index];
2130                 (f.in_flight, f.image_available)
2131             };
2132             self.core.device
2133                 .wait_for_fences(&[in_flight], true, u64::MAX)
2134                 .expect("Fence wait failed");
2135             // What this slot last carried has finished: the mesh buffers an
2136             // update replaced while those frames read them may be reused.
2137             self.scene.frame_waited(&self.core.device, self.core.allocator.as_mut().unwrap(), FRAMES_IN_FLIGHT as u64);
2138 
2139             // The first frame with a blur plate that will copy the frame so
2140             // far allocates the snapshot it copies into. Decided the way the
2141             // record below decides, except that a scene ever staged counts as
2142             // a backdrop (it may become valid while recording), which only
2143             // ever errs toward allocating.
2144             if !self.snapshot_wanted {
2145                 let assume_backdrop = self.scene.wanted || self.scene.backdrop_valid;
2146                 let exempt =
2147                     first_frost_exempt(frame2d.batches, frame2d.images, frame2d.clear_color, assume_backdrop);
2148                 if frame2d.batches.iter().enumerate().any(|(i, b)| b.blur_behind && Some(i) != exempt) {
2149                     self.snapshot_wanted = true;
2150                     self.sync_snapshot_target();
2151                 }
2152             }
2153 
2154             if present_debug() {
2155                 eprintln!("[vk] frame {} acquire...", self.present_debug_count);
2156             }
2157             let image_index = match self.swapchain_loader.acquire_next_image(
2158                 self.swapchain,
2159                 u64::MAX,
2160                 image_available,
2161                 vk::Fence::null(),
2162             ) {
2163                 Ok((index, suboptimal)) => {
2164                     if suboptimal {
2165                         self.swapchain_dirty = true;
2166                     }
2167                     index
2168                 }
2169                 Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
2170                     self.swapchain_dirty = true;
2171                     return false;
2172                 }
2173                 Err(result @ vk::Result::ERROR_SURFACE_LOST_KHR) => {
2174                     self.mark_surface_lost(SurfaceLost { call: "vkAcquireNextImageKHR", result });
2175                     return false;
2176                 }
2177                 Err(e) => {
2178                     log::error!("acquire_next_image failed: {e:?}");
2179                     return false;
2180                 }
2181             };
2182 
2183             self.core.device.reset_fences(&[in_flight]).unwrap();
2184 
2185             // Re-upload the bevel-profile LUT when it changed (a live ramp
2186             // edit). The other in-flight frame may still read the old bytes —
2187             // both are valid profiles, so the one-frame mix is benign.
2188             if self.profile_gen != crate::layout::bevel_profile_generation()
2189                 || self.roll_profile_gen != crate::layout::roll_profile_generation()
2190                 || self.relief_uploaded != self.relief_px()
2191             {
2192                 self.write_window_info();
2193             }
2194 
2195             // Upload this frame's plate carves into its slot of the feature
2196             // UBO (the slot's previous user has fenced, so no race).
2197             if !frame2d.plate_features.is_empty() {
2198                 let n = frame2d.plate_features.len().min(MAX_PLATE_FEATURES);
2199                 let base = frame_index * MAX_PLATE_FEATURES * PLATE_FEATURE_BYTES;
2200                 if let Some(allocation) = self.plate_features.allocation.as_mut() {
2201                     let bytes: &[u8] = bytemuck::cast_slice(&frame2d.plate_features[..n]);
2202                     allocation.mapped_slice_mut().unwrap()[base..base + bytes.len()]
2203                         .copy_from_slice(bytes);
2204                 }
2205             }
2206 
2207             // Upload display-list + overlay vertices into this frame's buffer
2208             // (its fence has signaled, so the GPU is done with it; growing swaps
2209             // in a fresh buffer). Overlay verts sit after the main range.
2210             let vert_bytes: &[u8] = bytemuck::cast_slice(frame2d.verts);
2211             let overlay_bytes: &[u8] = bytemuck::cast_slice(frame2d.overlay_verts);
2212             let needed = (vert_bytes.len() + overlay_bytes.len()) as vk::DeviceSize;
2213             if needed > self.frames[frame_index].vertex.size {
2214                 let mut old =
2215                     std::mem::replace(&mut self.frames[frame_index].vertex, AllocatedBuffer::null());
2216                 let allocator = self.core.allocator.as_mut().unwrap();
2217                 destroy_cpu_buffer(&self.core.device, allocator, &mut old);
2218                 self.frames[frame_index].vertex = create_cpu_buffer(
2219                     &self.core.device,
2220                     allocator,
2221                     needed.next_power_of_two(),
2222                     vk::BufferUsageFlags::VERTEX_BUFFER,
2223                     "vertices",
2224                 );
2225             }
2226             if needed > 0 {
2227                 let mapped = self.frames[frame_index]
2228                     .vertex
2229                     .allocation
2230                     .as_mut()
2231                     .unwrap()
2232                     .mapped_slice_mut()
2233                     .unwrap();
2234                 mapped[..vert_bytes.len()].copy_from_slice(vert_bytes);
2235                 mapped[vert_bytes.len()..vert_bytes.len() + overlay_bytes.len()]
2236                     .copy_from_slice(overlay_bytes);
2237             }
2238             self.frames[frame_index].vertex_count = frame2d.verts.len() as u32;
2239             self.frames[frame_index].overlay_start = frame2d.verts.len() as u32;
2240             self.frames[frame_index].overlay_count = frame2d.overlay_verts.len() as u32;
2241             self.text.write_frame_buffers(
2242                 &self.core.device,
2243                 self.core.allocator.as_mut().unwrap(),
2244                 frame_index,
2245             );
2246             self.image.process_pending(
2247                 &self.core.device,
2248                 self.core.allocator.as_mut().unwrap(),
2249                 self.core.queue,
2250                 self.core.command_pool,
2251             );
2252             self.image.write_frame_buffer(
2253                 &self.core.device,
2254                 self.core.allocator.as_mut().unwrap(),
2255                 frame_index,
2256                 frame2d.images,
2257                 self.extent,
2258             );
2259             let clip_radius = self.clip_corner_radius();
2260             self.scene.write_frame_uniforms(
2261                 &self.core.device,
2262                 self.core.allocator.as_mut().unwrap(),
2263                 frame_index,
2264                 clip_radius,
2265             );
2266             if let Some(rt) = self.rt.as_mut() {
2267                 let images = &self.image;
2268                 rt.write_frame_uniforms(&self.core.device, frame_index, &|id| {
2269                     images.view_and_size(id)
2270                 });
2271             }
2272 
2273             // Record.
2274             let cmd = self.frames[frame_index].cmd;
2275             self.core.device
2276                 .begin_command_buffer(cmd, &vk::CommandBufferBeginInfo::default())
2277                 .unwrap();
2278             self.text.record_upload(&self.core.device, cmd, frame_index);
2279 
2280             // Offscreen 3D pass (only when a scene was staged); leaves the
2281             // backdrop in TRANSFER_SRC.
2282             let mut scene_recorded =
2283                 self.scene.record(&self.core.device, cmd, frame_index, &self.image);
2284 
2285             // Path-tracer pass (only when staged via `stage_rt`): one
2286             // accumulation dispatch, blitted into the backdrop's pane region —
2287             // it fills the same slot as the raster scene pass and leaves the
2288             // backdrop in TRANSFER_SRC likewise.
2289             if let Some(rt) = self.rt.as_mut() {
2290                 let rt_recorded = rt.record(
2291                     &self.core.device,
2292                     cmd,
2293                     frame_index,
2294                     self.scene.backdrop_image,
2295                     self.extent,
2296                     scene_recorded,
2297                 );
2298                 if rt_recorded {
2299                     self.scene.backdrop_valid = true;
2300                     scene_recorded = true;
2301                 }
2302             }
2303 
2304             // With a valid backdrop, replay it under the UI: copy it into the
2305             // swapchain image and open the UI pass with LOAD instead of CLEAR.
2306             let use_backdrop = self.scene.backdrop_valid;
2307             if use_backdrop {
2308                 if !scene_recorded {
2309                     // Reused backdrop is in SHADER_READ_ONLY from last frame.
2310                     self.core.device.cmd_pipeline_barrier(
2311                         cmd,
2312                         vk::PipelineStageFlags::FRAGMENT_SHADER,
2313                         vk::PipelineStageFlags::TRANSFER,
2314                         vk::DependencyFlags::empty(),
2315                         &[],
2316                         &[],
2317                         &[vk::ImageMemoryBarrier::default()
2318                             .src_access_mask(vk::AccessFlags::SHADER_READ)
2319                             .dst_access_mask(vk::AccessFlags::TRANSFER_READ)
2320                             .old_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
2321                             .new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
2322                             .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
2323                             .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
2324                             .image(self.scene.backdrop_image)
2325                             .subresource_range(COLOR_RANGE)],
2326                     );
2327                 }
2328                 let swapchain_image = self.swapchain_images[image_index as usize];
2329                 self.core.device.cmd_pipeline_barrier(
2330                     cmd,
2331                     vk::PipelineStageFlags::TOP_OF_PIPE,
2332                     vk::PipelineStageFlags::TRANSFER,
2333                     vk::DependencyFlags::empty(),
2334                     &[],
2335                     &[],
2336                     &[vk::ImageMemoryBarrier::default()
2337                         .src_access_mask(vk::AccessFlags::empty())
2338                         .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
2339                         .old_layout(vk::ImageLayout::UNDEFINED)
2340                         .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
2341                         .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
2342                         .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
2343                         .image(swapchain_image)
2344                         .subresource_range(COLOR_RANGE)],
2345                 );
2346                 let subresource = vk::ImageSubresourceLayers::default()
2347                     .aspect_mask(vk::ImageAspectFlags::COLOR)
2348                     .layer_count(1);
2349                 self.core.device.cmd_copy_image(
2350                     cmd,
2351                     self.scene.backdrop_image,
2352                     vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
2353                     swapchain_image,
2354                     vk::ImageLayout::TRANSFER_DST_OPTIMAL,
2355                     &[vk::ImageCopy::default()
2356                         .src_subresource(subresource)
2357                         .dst_subresource(subresource)
2358                         .extent(vk::Extent3D {
2359                             width: self.extent.width,
2360                             height: self.extent.height,
2361                             depth: 1,
2362                         })],
2363                 );
2364                 // Backdrop back to sampleable for the UI pass's blur plates.
2365                 self.core.device.cmd_pipeline_barrier(
2366                     cmd,
2367                     vk::PipelineStageFlags::TRANSFER,
2368                     vk::PipelineStageFlags::FRAGMENT_SHADER,
2369                     vk::DependencyFlags::empty(),
2370                     &[],
2371                     &[],
2372                     &[vk::ImageMemoryBarrier::default()
2373                         .src_access_mask(vk::AccessFlags::TRANSFER_READ)
2374                         .dst_access_mask(vk::AccessFlags::SHADER_READ)
2375                         .old_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
2376                         .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
2377                         .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
2378                         .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
2379                         .image(self.scene.backdrop_image)
2380                         .subresource_range(COLOR_RANGE)],
2381                 );
2382             }
2383 
2384             let frame = &self.frames[frame_index];
2385             let clear_values = [vk::ClearValue {
2386                 color: vk::ClearColorValue { float32: frame2d.clear_color },
2387             }];
2388             let full_scissor = vk::Rect2D {
2389                 offset: vk::Offset2D { x: 0, y: 0 },
2390                 extent: self.extent,
2391             };
2392             // This frame's damage, and whether the acquired image can be
2393             // brought up to date by repainting part of it. Backdrop (3D
2394             // scene) frames copy whole images around and stay full; a
2395             // frosted plate grows the region instead (below).
2396             let damage = frame2d.damage.map(|(x, y, w, h)| {
2397                 rect_intersect(
2398                     vk::Rect2D {
2399                         offset: vk::Offset2D { x: x as i32, y: y as i32 },
2400                         extent: vk::Extent2D { width: w, height: h },
2401                     },
2402                     full_scissor,
2403                 )
2404             });
2405             let frost_exempt = first_frost_exempt(frame2d.batches, frame2d.images, frame2d.clear_color, use_backdrop);
2406             let mut partial: Option<vk::Rect2D> = match (damage, self.image_ages[image_index as usize]) {
2407                 _ if use_backdrop => None,
2408                 (Some(d), ImageAge::Current) => Some(d),
2409                 (Some(d), ImageAge::Behind(missing)) => Some(rect_union(d, missing)),
2410                 _ => None,
2411             };
2412             // A frosted plate's blur reads the snapshot of the frame so far,
2413             // and in a partial frame that snapshot is only right inside the
2414             // region — outside it the image holds the previous FINAL frame,
2415             // the plate and whatever covers it included. So a plate the
2416             // region touches is repainted whole, with everything its blur
2417             // can reach, and the grown region may touch the next plate.
2418             if let Some(mut region) = partial {
2419                 let frosts: Vec<vk::Rect2D> = frame2d
2420                     .batches
2421                     .iter()
2422                     .enumerate()
2423                     .filter(|&(i, b)| b.blur_behind && Some(i) != frost_exempt && b.start < b.end)
2424                     .filter_map(|(_, b)| {
2425                         let verts = frame2d.verts.get(b.start as usize..b.end as usize)?;
2426                         verts_bounds(verts, self.extent)
2427                     })
2428                     .collect();
2429                 loop {
2430                     let mut grew = false;
2431                     for &plate in &frosts {
2432                         let need = rect_intersect(rect_expand(plate, BLUR_REACH_PX), full_scissor);
2433                         if rect_overlaps(region, plate) && rect_intersect(region, need) != need {
2434                             region = rect_union(region, need);
2435                             grew = true;
2436                         }
2437                     }
2438                     if !grew {
2439                         break;
2440                     }
2441                 }
2442                 partial = Some(region);
2443             }
2444             // Every scissor of the frame passes through this.
2445             let clip = |r: vk::Rect2D| match partial {
2446                 Some(region) => rect_intersect(r, region),
2447                 None => r,
2448             };
2449             let (ui_pass, ui_clear_values): (vk::RenderPass, &[vk::ClearValue]) = if use_backdrop {
2450                 (self.render_pass_load, &[])
2451             } else if partial.is_some() {
2452                 (self.render_pass_partial, &[])
2453             } else {
2454                 (self.render_pass, &clear_values)
2455             };
2456             self.core.device.cmd_begin_render_pass(
2457                 cmd,
2458                 &vk::RenderPassBeginInfo::default()
2459                     .render_pass(ui_pass)
2460                     .framebuffer(self.framebuffers[image_index as usize])
2461                     .render_area(vk::Rect2D {
2462                         offset: vk::Offset2D { x: 0, y: 0 },
2463                         extent: self.extent,
2464                     })
2465                     .clear_values(ui_clear_values),
2466                 vk::SubpassContents::INLINE,
2467             );
2468             self.core.device
2469                 .cmd_set_viewport(cmd, 0, &[flipped_viewport(self.extent)]);
2470             self.core.device.cmd_set_scissor(cmd, 0, &[clip(full_scissor)]);
2471             if let Some(region) = partial {
2472                 // The partial pass loads instead of clearing; clear what it
2473                 // is about to repaint.
2474                 if region.extent.width > 0 && region.extent.height > 0 {
2475                     self.core.device.cmd_clear_attachments(
2476                         cmd,
2477                         &[vk::ClearAttachment::default()
2478                             .aspect_mask(vk::ImageAspectFlags::COLOR)
2479                             .color_attachment(0)
2480                             .clear_value(clear_values[0])],
2481                         &[vk::ClearRect::default().rect(region).layer_count(1)],
2482                     );
2483                 }
2484             }
2485             // Display-list geometry interleaved with user images: each image
2486             // quad draws before the vertex its `z_before` names, so it sits
2487             // above earlier geometry and below later geometry.
2488             {
2489                 let images = frame2d.images;
2490                 let mut order: Vec<usize> = (0..images.len()).collect();
2491                 order.sort_by_key(|&k| images[k].z_before);
2492                 let mut img_i = 0usize;
2493 
2494                 // Corner-shape exponent for the rounded-rect clip SDF, so clipped
2495                 // edges cut along the same squircle family as the tessellated and
2496                 // SDF-lit plate corners (a plate batch overwrites the slot with
2497                 // its own — identical — per-plate value).
2498                 let clip_shape = crate::layout::corner_shape();
2499 
2500                 let default_batch = [Batch2D {
2501                     scissor: None,
2502                     clip_rrect: None,
2503                     start: 0,
2504                     end: frame.vertex_count,
2505                     plate: None,
2506                     blur_behind: false,
2507                 }];
2508                 let batches: &[Batch2D] =
2509                     if frame2d.batches.is_empty() { &default_batch } else { frame2d.batches };
2510 
2511                 // Which @group(0) the vertex draws bind: the scene-backdrop set
2512                 // until the first blur-behind snapshot, the snapshot set after —
2513                 // so blur plates sample the frame-so-far, and later blur plates
2514                 // sample refreshed copies that include earlier ones.
2515                 let mut active_set = self.descriptor_set;
2516                 // CONSECUTIVE blur plates share one snapshot: only a non-blur
2517                 // draw invalidates it. A run of blur plates (the designer's
2518                 // node bodies) costs one copy, not one per plate — they don't
2519                 // see each other, which only matters where they overlap.
2520                 let mut snapshot_fresh = false;
2521 
2522                 for (batch_i, batch) in batches.iter().enumerate() {
2523                     // Images due at this batch's boundary draw first: they sit
2524                     // beneath the batch's geometry, and a blur snapshot taken
2525                     // for this batch must capture them (an image whose
2526                     // `z_before` equals the batch start would otherwise slip
2527                     // to after the snapshot and never be frosted).
2528                     while let Some(&k) = order.get(img_i) {
2529                         let q = &images[k];
2530                         if q.z_before > batch.start {
2531                             break;
2532                         }
2533                         img_i += 1;
2534                         let img_scissor = match q.clip {
2535                             Some((cx, cy, cw, ch)) => vk::Rect2D {
2536                                 offset: vk::Offset2D { x: cx as i32, y: cy as i32 },
2537                                 extent: vk::Extent2D {
2538                                     width: cw.min(self.extent.width.saturating_sub(cx)),
2539                                     height: ch.min(self.extent.height.saturating_sub(cy)),
2540                                 },
2541                             },
2542                             None => full_scissor,
2543                         };
2544                         self.core.device.cmd_set_scissor(cmd, 0, &[clip(img_scissor)]);
2545                         self.image.record_quad(&self.core.device, cmd, frame_index, k, q.image);
2546                         snapshot_fresh = false;
2547                     }
2548                     if batch.blur_behind && Some(batch_i) == frost_exempt {
2549                         // Nothing drawn yet: the zeroed backdrop the default
2550                         // set binds IS the frame so far (`first_frost_exempt`).
2551                     } else if batch.blur_behind {
2552                         if !snapshot_fresh {
2553                             self.snapshot_frame_so_far(cmd, image_index as usize);
2554                             active_set = self.descriptor_set_snapshot;
2555                             snapshot_fresh = true;
2556                         }
2557                     } else if batch.start < batch.end {
2558                         snapshot_fresh = false;
2559                     }
2560                     // Resolve the batch scissor; a degenerate one skips the
2561                     // vertex draws (images still process on their own clips).
2562                     let batch_scissor: Option<vk::Rect2D> = match batch.scissor {
2563                         Some((bx, by, bw, bh)) => {
2564                             if bx >= self.extent.width || by >= self.extent.height {
2565                                 None
2566                             } else {
2567                                 let bw = bw.min(self.extent.width - bx);
2568                                 let bh = bh.min(self.extent.height - by);
2569                                 if bw == 0 || bh == 0 {
2570                                     None
2571                                 } else {
2572                                     Some(vk::Rect2D {
2573                                         offset: vk::Offset2D { x: bx as i32, y: by as i32 },
2574                                         extent: vk::Extent2D { width: bw, height: bh },
2575                                     })
2576                                 }
2577                             }
2578                         }
2579                         None => Some(full_scissor),
2580                     };
2581 
2582                     let mut cursor = batch.start;
2583                     while cursor < batch.end {
2584                         let next_z =
2585                             order.get(img_i).map(|&k| images[k].z_before).unwrap_or(u32::MAX);
2586                         if next_z <= cursor {
2587                             let k = order[img_i];
2588                             img_i += 1;
2589                             let q = &images[k];
2590                             let img_scissor = match q.clip {
2591                                 Some((cx, cy, cw, ch)) => vk::Rect2D {
2592                                     offset: vk::Offset2D { x: cx as i32, y: cy as i32 },
2593                                     extent: vk::Extent2D {
2594                                         width: cw.min(self.extent.width.saturating_sub(cx)),
2595                                         height: ch.min(self.extent.height.saturating_sub(cy)),
2596                                     },
2597                                 },
2598                                 None => full_scissor,
2599                             };
2600                             self.core.device.cmd_set_scissor(cmd, 0, &[clip(img_scissor)]);
2601                             self.image.record_quad(&self.core.device, cmd, frame_index, k, q.image);
2602                             continue;
2603                         }
2604                         let upto = next_z.min(batch.end);
2605                         if let Some(scissor) = batch_scissor {
2606                             self.core.device
2607                                 .cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline);
2608                             self.core.device.cmd_bind_descriptor_sets(
2609                                 cmd,
2610                                 vk::PipelineBindPoint::GRAPHICS,
2611                                 self.pipeline_layout,
2612                                 0,
2613                                 &[active_set],
2614                                 &[],
2615                             );
2616                             self.core.device
2617                                 .cmd_bind_vertex_buffers(cmd, 0, &[frame.vertex.buffer], &[0]);
2618                             self.core.device.cmd_set_scissor(cmd, 0, &[clip(scissor)]);
2619                             // Per-batch rounded-rect clip (fragments outside
2620                             // discard) + the SDF-lit plate block when this
2621                             // batch is a plate cover quad.
2622                             let pc = batch_push_constants(batch, clip_shape, frame_index * MAX_PLATE_FEATURES);
2623                             self.core.device.cmd_push_constants(
2624                                 cmd,
2625                                 self.pipeline_layout,
2626                                 vk::ShaderStageFlags::FRAGMENT,
2627                                 0,
2628                                 bytemuck::cast_slice(&pc),
2629                             );
2630                             self.core.device.cmd_draw(cmd, upto - cursor, 1, cursor, 0);
2631                         }
2632                         cursor = upto;
2633                     }
2634                 }
2635                 // Images sorting after all geometry.
2636                 while let Some(&k) = order.get(img_i) {
2637                     img_i += 1;
2638                     let q = &images[k];
2639                     let img_scissor = match q.clip {
2640                         Some((cx, cy, cw, ch)) => vk::Rect2D {
2641                             offset: vk::Offset2D { x: cx as i32, y: cy as i32 },
2642                             extent: vk::Extent2D {
2643                                 width: cw.min(self.extent.width.saturating_sub(cx)),
2644                                 height: ch.min(self.extent.height.saturating_sub(cy)),
2645                             },
2646                         },
2647                         None => full_scissor,
2648                     };
2649                     self.core.device.cmd_set_scissor(cmd, 0, &[clip(img_scissor)]);
2650                     self.image.record_quad(&self.core.device, cmd, frame_index, k, q.image);
2651                 }
2652                 // Restore for the text/overlay draws.
2653                 self.core.device.cmd_set_scissor(cmd, 0, &[clip(full_scissor)]);
2654             }
2655             self.text.record_draw(&self.core.device, cmd, frame_index);
2656             if frame.overlay_count > 0 {
2657                 // The text pass bound its own pipeline; rebind for the overlay.
2658                 self.core.device
2659                     .cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline);
2660                 self.core.device.cmd_bind_descriptor_sets(
2661                     cmd,
2662                     vk::PipelineBindPoint::GRAPHICS,
2663                     self.pipeline_layout,
2664                     0,
2665                     &[self.descriptor_set],
2666                     &[],
2667                 );
2668                 self.core.device
2669                     .cmd_bind_vertex_buffers(cmd, 0, &[frame.vertex.buffer], &[0]);
2670                 // Push constants persist across binds — clear any batch's
2671                 // rounded clip and plate mode.
2672                 let pc = [0.0f32; PUSH_CONSTANT_FLOATS];
2673                 self.core.device.cmd_push_constants(
2674                     cmd,
2675                     self.pipeline_layout,
2676                     vk::ShaderStageFlags::FRAGMENT,
2677                     0,
2678                     bytemuck::cast_slice(&pc),
2679                 );
2680                 self.core.device
2681                     .cmd_draw(cmd, frame.overlay_count, 1, frame.overlay_start, 0);
2682             }
2683             self.core.device.cmd_end_render_pass(cmd);
2684             self.core.device.end_command_buffer(cmd).unwrap();
2685 
2686             // Submit + present. The acquire semaphore gates the swapchain image's
2687             // first use: the backdrop copy (TRANSFER) or the UI pass (COLOR).
2688             let wait_semaphores = [image_available];
2689             let wait_stages = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
2690                 | vk::PipelineStageFlags::TRANSFER];
2691             let cmds = [cmd];
2692             let signal_semaphores = [self.render_finished[image_index as usize]];
2693             let submit = vk::SubmitInfo::default()
2694                 .wait_semaphores(&wait_semaphores)
2695                 .wait_dst_stage_mask(&wait_stages)
2696                 .command_buffers(&cmds)
2697                 .signal_semaphores(&signal_semaphores);
2698             self.core.device
2699                 .queue_submit(self.core.queue, &[submit], in_flight)
2700                 .expect("Queue submit failed");
2701             self.scene.submitted += 1;
2702 
2703             // The image now holds this frame; every other image fell behind
2704             // by this frame's damage.
2705             for (k, age) in self.image_ages.iter_mut().enumerate() {
2706                 *age = if k == image_index as usize {
2707                     ImageAge::Current
2708                 } else {
2709                     match (damage, *age) {
2710                         (Some(d), ImageAge::Current) => ImageAge::Behind(d),
2711                         (Some(d), ImageAge::Behind(m)) => ImageAge::Behind(rect_union(d, m)),
2712                         _ => ImageAge::Unknown,
2713                     }
2714                 };
2715             }
2716 
2717             let swapchains = [self.swapchain];
2718             let image_indices = [image_index];
2719             // What changed since the previous present — the frame's damage,
2720             // however much of the image had to be repainted to get there.
2721             // Only for a frame that was itself partial: the first frames of
2722             // a swapchain replace a buffer of another size or none at all.
2723             let present_rects = [vk::RectLayerKHR::default()
2724                 .offset(damage.unwrap_or(full_scissor).offset)
2725                 .extent(damage.unwrap_or(full_scissor).extent)
2726                 .layer(0)];
2727             let present_region = [vk::PresentRegionKHR::default().rectangles(&present_rects)];
2728             let mut present_regions = vk::PresentRegionsKHR::default().regions(&present_region);
2729             let mut present = vk::PresentInfoKHR::default()
2730                 .wait_semaphores(&signal_semaphores)
2731                 .swapchains(&swapchains)
2732                 .image_indices(&image_indices);
2733             if self.core.incremental_present && partial.is_some() {
2734                 present = present.push_next(&mut present_regions);
2735             }
2736             if present_debug() {
2737                 eprintln!("[vk] frame {} present img {}...", self.present_debug_count, image_index);
2738             }
2739             match self.swapchain_loader.queue_present(self.core.queue, &present) {
2740                 Ok(suboptimal) => {
2741                     if suboptimal {
2742                         self.swapchain_dirty = true;
2743                     }
2744                 }
2745                 Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
2746                     self.swapchain_dirty = true;
2747                 }
2748                 Err(result @ vk::Result::ERROR_SURFACE_LOST_KHR) => {
2749                     self.mark_surface_lost(SurfaceLost { call: "vkQueuePresentKHR", result });
2750                 }
2751                 Err(e) => log::error!("queue_present failed: {e:?}"),
2752             }
2753 
2754             if present_debug() {
2755                 eprintln!("[vk] frame {} presented", self.present_debug_count);
2756                 self.present_debug_count += 1;
2757             }
2758             self.frame_index = (self.frame_index + 1) % FRAMES_IN_FLIGHT;
2759         }
2760         true
2761     }
2762 }
2763 
2764 /// `CCE_PRESENT_DEBUG=1` traces every acquire/present to stderr — the
2765 /// diagnostic for present-pipeline stalls (a present that logs `acquire...`
2766 /// or `present img N...` with no matching completion line is blocked inside
2767 /// the driver; see the off-viewport freeze notes on the present-mode choice
2768 /// in `create_swapchain`).
2769 pub(crate) fn present_debug() -> bool {
2770     static FLAG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2771     *FLAG.get_or_init(|| std::env::var_os("CCE_PRESENT_DEBUG").is_some())
2772 }
2773 
2774 /// The 3D half of the renderer, as an app stages it through
2775 /// `Application::init_3d` / `stage_3d` — each method is the inherent one.
2776 impl crate::draw::scene::Stage3D for VkRenderer {
2777     fn create_mesh(&mut self, verts: &[Vertex3D]) -> MeshId {
2778         VkRenderer::create_mesh(self, verts)
2779     }
2780     fn update_mesh(&mut self, id: MeshId, verts: &[Vertex3D]) {
2781         VkRenderer::update_mesh(self, id, verts)
2782     }
2783     fn stage_scene(&mut self, scissor: (u32, u32, u32, u32), draws: Vec<SceneDraw>) {
2784         VkRenderer::stage_scene(self, scissor, draws)
2785     }
2786     fn stage_scene_images(&mut self, images: Vec<SceneImage>) {
2787         VkRenderer::stage_scene_images(self, images)
2788     }
2789     fn set_scene_light(&mut self, toward: [f32; 3]) {
2790         VkRenderer::set_scene_light(self, toward)
2791     }
2792     fn set_rt_scene_with_image(&mut self, triangles: &[RtTriangle], materials: &[RtMaterial], image: Option<RtImage>) {
2793         VkRenderer::set_rt_scene_with_image(self, triangles, materials, image)
2794     }
2795     fn set_rt_scene_prepared(&mut self, scene: &PreparedRtScene) {
2796         VkRenderer::set_rt_scene_prepared(self, scene)
2797     }
2798     fn rt_needs_bvh(&self) -> bool {
2799         VkRenderer::rt_needs_bvh(self)
2800     }
2801     fn set_rt_environment(&mut self, environment: RtEnvironment) {
2802         VkRenderer::set_rt_environment(self, environment)
2803     }
2804     fn set_rt_background(&mut self, color: Option<[f32; 3]>) {
2805         VkRenderer::set_rt_background(self, color)
2806     }
2807     fn stage_rt(&mut self, pane: (u32, u32, u32, u32), camera: RtCamera) {
2808         VkRenderer::stage_rt(self, pane, camera)
2809     }
2810     fn rt_accumulating(&self) -> bool {
2811         VkRenderer::rt_accumulating(self)
2812     }
2813     fn lit(&mut self) -> Option<&mut dyn crate::draw::lit::LitStage3D> {
2814         Some(self)
2815     }
2816 }
2817 
2818 impl crate::draw::lit::LitStage3D for VkRenderer {
2819     fn create_lit_mesh(&mut self, verts: &[crate::draw::lit::LitVertex]) -> crate::draw::lit::LitMeshId {
2820         VkRenderer::create_lit_mesh(self, verts)
2821     }
2822     fn update_lit_mesh(&mut self, id: crate::draw::lit::LitMeshId, verts: &[crate::draw::lit::LitVertex]) {
2823         VkRenderer::update_lit_mesh(self, id, verts)
2824     }
2825     fn set_lit_light(&mut self, light: crate::draw::lit::LitLight) {
2826         VkRenderer::set_lit_light(self, light)
2827     }
2828     fn stage_lit(&mut self, draws: Vec<crate::draw::lit::LitDraw>) {
2829         VkRenderer::stage_lit(self, draws)
2830     }
2831 }
2832 
2833 impl Drop for VkRenderer {
2834     fn drop(&mut self) {
2835         unsafe {
2836             let _ = self.core.device.device_wait_idle();
2837 
2838             let mut frames = std::mem::take(&mut self.frames);
2839             for frame in &mut frames {
2840                 self.core.device.destroy_semaphore(frame.image_available, None);
2841                 self.core.device.destroy_fence(frame.in_flight, None);
2842                 let mut vertex = std::mem::replace(&mut frame.vertex, AllocatedBuffer::null());
2843                 if let Some(allocator) = self.core.allocator.as_mut() {
2844                     destroy_cpu_buffer(&self.core.device, allocator, &mut vertex);
2845                 }
2846             }
2847 
2848             self.destroy_swapchain_resources();
2849             if self.swapchain != vk::SwapchainKHR::null() {
2850                 self.swapchain_loader.destroy_swapchain(self.swapchain, None);
2851             }
2852 
2853             if let Some(allocator) = self.core.allocator.as_mut() {
2854                 self.text.destroy(&self.core.device, allocator);
2855             }
2856 
2857             self.core.device.destroy_sampler(self.backdrop_sampler, None);
2858             if self.snapshot_view != vk::ImageView::null() {
2859                 self.core.device.destroy_image_view(self.snapshot_view, None);
2860                 self.core.device.destroy_image(self.snapshot_image, None);
2861             }
2862             if let Some(alloc) = self.snapshot_allocation.take() {
2863                 if let Some(allocator) = self.core.allocator.as_mut() {
2864                     let _ = allocator.free(alloc);
2865                 }
2866             }
2867             if let Some(allocator) = self.core.allocator.as_mut() {
2868                 self.scene.destroy(&self.core.device, allocator);
2869                 self.image.destroy(&self.core.device, allocator);
2870                 if let Some(mut rt) = self.rt.take() {
2871                     rt.destroy(&self.core.device, allocator);
2872                 }
2873             }
2874             let mut window_info = std::mem::replace(&mut self.window_info, AllocatedBuffer::null());
2875             let mut plate_features =
2876                 std::mem::replace(&mut self.plate_features, AllocatedBuffer::null());
2877             if let Some(allocator) = self.core.allocator.as_mut() {
2878                 destroy_cpu_buffer(&self.core.device, allocator, &mut window_info);
2879                 destroy_cpu_buffer(&self.core.device, allocator, &mut plate_features);
2880             }
2881 
2882             self.core.device.destroy_descriptor_pool(self.descriptor_pool, None);
2883             self.core.device
2884                 .destroy_descriptor_set_layout(self.descriptor_set_layout, None);
2885             self.core.device.destroy_pipeline(self.pipeline, None);
2886             self.core.device.destroy_pipeline_layout(self.pipeline_layout, None);
2887             self.core.device.destroy_shader_module(self.shader_module, None);
2888             self.core.device.destroy_render_pass(self.render_pass, None);
2889             self.core.device.destroy_render_pass(self.render_pass_load, None);
2890             self.core.device.destroy_render_pass(self.render_pass_partial, None);
2891             self.core.surface_loader.destroy_surface(self.surface, None);
2892             // The rest (allocator, command pool, device, instance) is the
2893             // core's Drop, which runs after this body.
2894         }
2895     }
2896 }
2897 
2898 #[cfg(test)]
2899 mod tests {
2900     /// The WGSL shaders compile at process start, so a syntax or validation
2901     /// error is a runtime panic in every client — catch it headlessly here.
2902     #[test]
2903     fn shader2d_compiles() {
2904         assert!(!super::shader2d_spirv().is_empty());
2905     }
2906 
2907     /// The blur snapshot halves down to a single texel or to the cap, so a
2908     /// tiny surface is not asked for levels it cannot have, and a large one
2909     /// does not build levels no stride reads.
2910     #[test]
2911     fn the_blur_snapshot_has_as_many_levels_as_the_widest_stride_reads() {
2912         use super::{snapshot_levels, SNAPSHOT_LEVELS_MAX};
2913         let e = |width, height| ash::vk::Extent2D { width, height };
2914         assert_eq!(snapshot_levels(e(1, 1)), 1);
2915         assert_eq!(snapshot_levels(e(2, 1)), 2);
2916         assert_eq!(snapshot_levels(e(40, 7)), 6);
2917         assert_eq!(snapshot_levels(e(2560, 1600)), SNAPSHOT_LEVELS_MAX);
2918         // The coarsest level's texel covers a 64 px stride.
2919         assert_eq!(1 << (SNAPSHOT_LEVELS_MAX - 1), 64);
2920     }
2921 
2922     /// The WebGPU variants validate with no capabilities at all — WebGPU has
2923     /// no push constants — and the 2D one carries its block as the uniform
2924     /// the web renderer binds. naga does not see everything a browser's
2925     /// compiler rejects (its derivative-uniformity analysis does not follow
2926     /// calls), so the browser probe is the last word; this catches a
2927     /// substitution that silently stopped applying.
2928     #[test]
2929     fn the_webgpu_shaders_validate_without_push_constants() {
2930         let web2d = crate::draw::shaders::shader2d_for_webgpu();
2931         assert!(!web2d.contains("var<push_constant>"));
2932         assert!(web2d.contains("@group(1) @binding(0) var<uniform> rrect_clip: RRectClip;"));
2933         for (name, src) in [("shader2d (web)", web2d.as_str()), ("glyph", crate::draw::shaders::GLYPH)] {
2934             let module = naga::front::wgsl::parse_str(src).unwrap_or_else(|e| panic!("{name}: {}", e.emit_to_string(src)));
2935             naga::valid::Validator::new(naga::valid::ValidationFlags::all(), naga::valid::Capabilities::empty())
2936                 .validate(&module)
2937                 .unwrap_or_else(|e| panic!("{name} does not validate for WebGPU: {e:?}"));
2938         }
2939         // And the block's size is what the renderers lay out.
2940         let module = naga::front::wgsl::parse_str(&web2d).unwrap();
2941         let block = module.types.iter().find(|(_, t)| t.name.as_deref() == Some("RRectClip")).expect("RRectClip").1;
2942         let naga::TypeInner::Struct { span, .. } = block.inner else { panic!("RRectClip is a struct") };
2943         assert_eq!(span as usize, crate::draw::PUSH_CONSTANT_FLOATS * 4);
2944         assert!(span as usize <= crate::draw::shaders::WEBGPU_BLOCK_STRIDE);
2945     }
2946 
2947     #[test]
2948     fn scene3d_compiles() {
2949         assert!(!super::scene3d_spirv().is_empty());
2950     }
2951 
2952     #[test]
2953     fn scene3d_image_compiles() {
2954         assert!(!super::scene3d_image_spirv().is_empty());
2955     }
2956 
2957     /// `WINDOW_INFO_BYTES` sizes the uniform buffer AND its descriptor range,
2958     /// and `write_window_info` addresses it by float index — all three have to
2959     /// agree with shader2d's `WindowInfo` struct, and nothing but a comment
2960     /// said so. A field appended to the WGSL without growing the const writes
2961     /// the new value past the end of the buffer, which is a validation error
2962     /// on a good day and a garbage uniform on a bad one.
2963     ///
2964     /// Reads the struct out of the shader source rather than duplicating its
2965     /// shape here, so it measures the thing it is guarding.
2966     /// build.rs compiles the fixed shaders with its own copy of
2967     /// `compile_wgsl`'s options. If the two drift, the embedded SPIR-V is no
2968     /// longer what this crate means by the shader; compare word for word.
2969     #[test]
2970     fn precompiled_spirv_matches_runtime_compile() {
2971         let cases: [(&str, &str, fn() -> &'static [u32]); 5] = [
2972             ("shader2d", crate::draw::shaders::SHADER2D, super::shader2d_spirv),
2973             ("glyph", crate::draw::shaders::GLYPH, super::glyph_spirv),
2974             ("scene3d", crate::draw::shaders::SCENE3D, super::scene3d_spirv),
2975             ("scene3d_image", crate::draw::shaders::SCENE3D_IMAGE, super::scene3d_image_spirv),
2976             ("scene3d_lit", crate::draw::shaders::SCENE3D_LIT, super::scene3d_lit_spirv),
2977         ];
2978         for (name, source, precompiled) in cases {
2979             assert!(
2980                 super::compile_wgsl(source) == precompiled(),
2981                 "{name}: build.rs output differs from compile_wgsl"
2982             );
2983         }
2984     }
2985 
2986     #[test]
2987     fn window_info_layout_matches_the_uniform_size() {
2988         let src = crate::draw::shaders::SHADER2D;
2989         let body = src
2990             .split_once("struct WindowInfo {")
2991             .expect("WindowInfo moved; this test scans for it")
2992             .1
2993             .split_once("\n}")
2994             .expect("unterminated WindowInfo")
2995             .0;
2996 
2997         let mut floats = 0usize;
2998         for line in body.lines() {
2999             let line = line.trim();
3000             if line.is_empty() || line.starts_with("//") {
3001                 continue;
3002             }
3003             let ty = line.split_once(':').expect("field: type").1.trim().trim_end_matches(',');
3004             floats += match ty {
3005                 "f32" => 1,
3006                 "vec2<f32>" | "vec2f" => 2,
3007                 "vec4<f32>" | "vec4f" => 4,
3008                 // std140-ish: an array of vec4 is its element count x 4.
3009                 t if t.starts_with("array<vec4f,") => {
3010                     let n: usize = t
3011                         .trim_start_matches("array<vec4f,")
3012                         .trim_end_matches('>')
3013                         .trim()
3014                         .parse()
3015                         .expect("array length");
3016                     n * 4
3017                 }
3018                 other => panic!("WindowInfo field type {other} is not in this test's size table"),
3019             };
3020         }
3021 
3022         assert_eq!(
3023             floats * 4,
3024             super::WINDOW_INFO_BYTES as usize,
3025             "WindowInfo is {floats} floats ({} bytes); WINDOW_INFO_BYTES says {}",
3026             floats * 4,
3027             super::WINDOW_INFO_BYTES,
3028         );
3029     }
3030 }
3031 
3032 #[cfg(test)]
3033 mod frost_exempt_tests {
3034     use super::*;
3035 
3036     fn batch(start: u32, end: u32, blur: bool) -> Batch2D {
3037         Batch2D { scissor: None, clip_rrect: None, start, end, plate: None, blur_behind: blur }
3038     }
3039 
3040     /// Only a frosted batch that is the first thing drawn, over a
3041     /// transparent clear with no scene, may skip its snapshot.
3042     #[test]
3043     fn only_the_first_frost_over_nothing_is_exempt() {
3044         let clear = [0.0; 4];
3045         let root_first = [batch(0, 6, true), batch(6, 12, true)];
3046         assert_eq!(first_frost_exempt(&root_first, &[], clear, false), Some(0));
3047         // An empty leading batch does not count as drawing.
3048         assert_eq!(first_frost_exempt(&[batch(0, 0, false), batch(0, 6, true)], &[], clear, false), Some(1));
3049         // Something drawn first, a scene backdrop, or an opaque clear: no.
3050         assert_eq!(first_frost_exempt(&[batch(0, 6, false), batch(6, 12, true)], &[], clear, false), None);
3051         assert_eq!(first_frost_exempt(&root_first, &[], clear, true), None);
3052         assert_eq!(first_frost_exempt(&root_first, &[], [0.0, 0.0, 0.0, 1.0], false), None);
3053         let image = crate::draw::ImageQuad { image: 1, rect: (0.0, 0.0, 1.0, 1.0), alpha: 1.0, z_before: 0, clip: None };
3054         assert_eq!(first_frost_exempt(&root_first, &[image], clear, false), None);
3055     }
3056 }