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

src/vk/renderer.rs (98.8K)

   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::image::{ImageQuad, ImageStage};
  21 use super::rt::{RtCamera, RtMaterial, RtStage, RtTriangle};
  22 use super::scene::{MeshId, SceneDraw, SceneStage, Vertex3D};
  23 use super::text::{TextSpan, TextStage};
  24 
  25 /// One scissored draw range of a 2D frame. `scissor` is (x, y, w, h) in
  26 /// physical pixels; None draws with the full-surface scissor. `clip_rrect` is an
  27 /// optional rounded-rect clip `[cx, cy, bx, by, r]` (center, SDF half-extents, corner
  28 /// radius; physical px) applied via push constants — fragments outside it discard, so a
  29 /// plate's children cut off at its rounded corners.
  30 pub struct Batch2D {
  31     pub scissor: Option<(u32, u32, u32, u32)>,
  32     pub clip_rrect: Option<[f32; 5]>,
  33     pub start: u32,
  34     pub end: u32,
  35     /// When set, this batch is a single SDF-lit plate cover quad: the params go
  36     /// out as push constants and shader2d's plate branch lights it per pixel.
  37     pub plate: Option<PlatePush>,
  38     /// A blur-behind plate (negative-alpha color): the renderer suspends the UI
  39     /// pass, copies the swapchain-so-far into its snapshot image, and resumes —
  40     /// so the plate's blur samples everything painted beneath it (background,
  41     /// widgets, wires), not just the 3D scene backdrop.
  42     pub blur_behind: bool,
  43 }
  44 
  45 /// Floats in the fragment push-constant block: the rounded-rect clip (`rect0`,
  46 /// `rect1` — 6 clip/flag floats plus the plate mode and corner shape) followed
  47 /// by [`PlatePush`]'s six vec4s. Field for field, this is shader2d's `RRectClip`.
  48 pub(crate) const PUSH_CONSTANT_FLOATS: usize = 32;
  49 
  50 /// The block in bytes. **This is exactly `maxPushConstantsSize`'s
  51 /// Vulkan-guaranteed minimum, so the budget is full** — every one of the 32
  52 /// slots is written. That is why a new SDF mode reinterprets existing fields per
  53 /// mode (5 reads `p_rect` as centre + radius, 6/7 as centre + radius + wedge
  54 /// angle, 8 as centre + half-width with `p_radii.xy` a normal) instead of adding
  55 /// one: there is nothing left to add.
  56 ///
  57 /// A block over 128 bytes is not portable by construction — 128 is the floor
  58 /// every conformant implementation must offer, and plenty of drivers offer no
  59 /// more. So growing this means querying `limits.max_push_constants_size` at
  60 /// device init and having a real fallback (a uniform buffer, or splitting the
  61 /// block), not just raising the number. The assertion below is the tripwire: a
  62 /// runtime check would be dead code today, because at exactly 128 it can never
  63 /// fire on a conformant device.
  64 pub(crate) const PUSH_CONSTANT_BYTES: u32 = (PUSH_CONSTANT_FLOATS * 4) as u32;
  65 
  66 const _: () = assert!(
  67     PUSH_CONSTANT_BYTES <= 128,
  68     "the push-constant block has outgrown the 128-byte Vulkan-guaranteed minimum: \
  69      query limits.max_push_constants_size at device init and add a fallback path \
  70      before raising PUSH_CONSTANT_FLOATS"
  71 );
  72 
  73 /// Push-constant block for one SDF-lit plate batch (physical px throughout).
  74 /// Mirrors the `p_*` fields of shader2d's `RRectClip`.
  75 #[derive(Clone, Copy, PartialEq, Debug)]
  76 pub struct PlatePush {
  77     /// SDF box: center + half-extents. May extend past the cover quad — that is
  78     /// how a recess suppresses a wall.
  79     pub rect: [f32; 4],
  80     /// Per-corner radii [tl, tr, br, bl].
  81     pub radii: [f32; 4],
  82     /// xyz = unit vector toward the light (+z out of the screen), w = roll width px.
  83     pub light: [f32; 4],
  84     /// [shading strength, specular strength, shininess, curvature/AO strength].
  85     pub material: [f32; 4],
  86     /// Mode 1: `[feature offset, feature count, frost z, frost w]` — xy into
  87     /// the frame's `plate_features`, the carves CSG'd out of this plate (the
  88     /// renderer adds the frame slot's base offset at record time); zw the
  89     /// plate's frost recipe, `scene::material::Frost::pack` (compression and
  90     /// refraction packed in z, the blur sigma in physical px in w). Mode 14 uses the same
  91     /// `[offset, count]` for the union's boxes. Mode 2: the host-plate box
  92     /// (center + half-extents) a free recess fades out against; far-away sides
  93     /// (±1e5) disable the fade.
  94     pub host: [f32; 4],
  95     /// RGB multiplies the roll's specular color (w unused). Neutral white
  96     /// normally; the focused-pane bevel carries the highlight color here.
  97     pub specular_tint: [f32; 4],
  98     /// 1.0 = raised lit plate, 2.0 = recess overlay, 3.0 = boss, 4.0 = ridge,
  99     /// 5.0 = sphere, 6.0/7.0 = concave fillet (recessed/raised), 8.0 = groove
 100     /// (slab carve about a line: `rect` = [cx, cy, half-width, _], `radii.xy` =
 101     /// the line's unit normal, `host` = the surface it is engraved into),
 102     /// 9.0 = trough, 10.0 = droplet (`radii` = [sag, belly r, belly half-w,
 103     /// blend k] px, `host` = [sheet corner r px, clarity, dome amplitude,
 104     /// attach r px], `material.w` = fresnel rim, `specular_tint` = [core
 105     /// density, _, _, bottom-bow rise px] — droplet glints are always white,
 106     /// so the tint RGB is repurposed; see shader2d's MODE_DROPLET).
 107     pub mode: f32,
 108     /// Corner shape exponent: 2.0 = circular arcs, > 2 = superellipse
 109     /// (continuous-curvature) corners — see shader2d's `plate_sdf_grad`.
 110     pub shape: f32,
 111 }
 112 
 113 /// A full 2D frame: the display-list vertices (optionally split into scissored
 114 /// batches), overlay vertices drawn after text, and the clear color (linear;
 115 /// only used on frames without a backdrop copy).
 116 pub struct Frame2D<'a> {
 117     pub verts: &'a [Vertex],
 118     pub batches: &'a [Batch2D],
 119     pub overlay_verts: &'a [Vertex],
 120     /// User images drawn interleaved with `verts` by each quad's `z_before`.
 121     pub images: &'a [ImageQuad],
 122     /// Carves CSG'd into this frame's SDF-lit plates, 12 floats each (rect
 123     /// center+half-extents, per-corner radii, [width px, depth px, 0, 0]).
 124     /// Plate batches reference them by offset+count in `PlatePush::host`.
 125     pub plate_features: &'a [[f32; 12]],
 126     pub clear_color: [f32; 4],
 127 }
 128 
 129 const FRAMES_IN_FLIGHT: usize = 2;
 130 /// Max plate-carve features per frame; the shader's UBO holds one slot of this
 131 /// size per frame in flight.
 132 pub const MAX_PLATE_FEATURES: usize = 64;
 133 const PLATE_FEATURE_BYTES: usize = 48;
 134 /// shader2d's WindowInfo UBO: [size/clip vec4][bevel-profile meta vec4]
 135 /// [8 vec4 of profile slope samples].
 136 // [size/clip vec4][carve profile meta + 8 vec4][roll profile meta + 8 vec4].
 137 // [size/clip vec4][carve profile meta][8 carve slopes][roll profile meta]
 138 // [8 roll slopes][relief heights][backdrop meta] = 21 vec4. Grows only at the
 139 // END — every offset above is addressed by index from both sides.
 140 const WINDOW_INFO_BYTES: vk::DeviceSize = 320;
 141 
 142 pub(crate) struct AllocatedBuffer {
 143     pub(crate) buffer: vk::Buffer,
 144     pub(crate) allocation: Option<Allocation>,
 145     pub(crate) size: vk::DeviceSize,
 146 }
 147 
 148 impl AllocatedBuffer {
 149     pub(crate) fn null() -> Self {
 150         AllocatedBuffer { buffer: vk::Buffer::null(), allocation: None, size: 0 }
 151     }
 152 }
 153 
 154 /// Create a host-visible buffer bound to gpu-allocator memory.
 155 pub(crate) fn create_cpu_buffer(
 156     device: &ash::Device,
 157     allocator: &mut Allocator,
 158     size: vk::DeviceSize,
 159     usage: vk::BufferUsageFlags,
 160     name: &str,
 161 ) -> AllocatedBuffer {
 162     unsafe {
 163         let buffer = device
 164             .create_buffer(
 165                 &vk::BufferCreateInfo::default()
 166                     .size(size)
 167                     .usage(usage)
 168                     .sharing_mode(vk::SharingMode::EXCLUSIVE),
 169                 None,
 170             )
 171             .expect("Failed to create buffer");
 172         let requirements = device.get_buffer_memory_requirements(buffer);
 173         let allocation = allocator
 174             .allocate(&AllocationCreateDesc {
 175                 name,
 176                 requirements,
 177                 location: MemoryLocation::CpuToGpu,
 178                 linear: true,
 179                 allocation_scheme: AllocationScheme::GpuAllocatorManaged,
 180             })
 181             .expect("Failed to allocate buffer memory");
 182         device
 183             .bind_buffer_memory(buffer, allocation.memory(), allocation.offset())
 184             .expect("Failed to bind buffer memory");
 185         AllocatedBuffer { buffer, allocation: Some(allocation), size }
 186     }
 187 }
 188 
 189 /// Destroy a buffer and return its memory to the allocator.
 190 pub(crate) fn destroy_cpu_buffer(
 191     device: &ash::Device,
 192     allocator: &mut Allocator,
 193     buf: &mut AllocatedBuffer,
 194 ) {
 195     unsafe {
 196         self::destroy_buffer_handle(device, buf.buffer);
 197     }
 198     if let Some(allocation) = buf.allocation.take() {
 199         let _ = allocator.free(allocation);
 200     }
 201     buf.buffer = vk::Buffer::null();
 202     buf.size = 0;
 203 }
 204 
 205 unsafe fn destroy_buffer_handle(device: &ash::Device, buffer: vk::Buffer) {
 206     if buffer != vk::Buffer::null() {
 207         device.destroy_buffer(buffer, None);
 208     }
 209 }
 210 
 211 struct Frame {
 212     cmd: vk::CommandBuffer,
 213     image_available: vk::Semaphore,
 214     in_flight: vk::Fence,
 215     vertex: AllocatedBuffer,
 216     vertex_count: u32,
 217     overlay_start: u32,
 218     overlay_count: u32,
 219 }
 220 
 221 pub struct VkRenderer {
 222     surface: vk::SurfaceKHR,
 223 
 224     swapchain_loader: ash::khr::swapchain::Device,
 225     swapchain: vk::SwapchainKHR,
 226     surface_format: vk::SurfaceFormatKHR,
 227     extent: vk::Extent2D,
 228     swapchain_images: Vec<vk::Image>,
 229     swapchain_views: Vec<vk::ImageView>,
 230     framebuffers: Vec<vk::Framebuffer>,
 231     // One per swapchain image (not per frame in flight): present waits on the
 232     // semaphore tied to the image being presented.
 233     render_finished: Vec<vk::Semaphore>,
 234 
 235     render_pass: vk::RenderPass,
 236     /// UI pass over a backdrop copy: loadOp LOAD, initial layout TRANSFER_DST.
 237     /// Framebuffers are shared with `render_pass` (compatible attachments).
 238     render_pass_load: vk::RenderPass,
 239     descriptor_set_layout: vk::DescriptorSetLayout,
 240     pipeline_layout: vk::PipelineLayout,
 241     pipeline: vk::Pipeline,
 242     shader_module: vk::ShaderModule,
 243 
 244     descriptor_pool: vk::DescriptorPool,
 245     descriptor_set: vk::DescriptorSet,
 246     /// Twin of `descriptor_set` with binding 0 pointing at `snapshot_image`
 247     /// instead of the scene backdrop; bound for every draw after the first
 248     /// mid-pass snapshot so blur plates sample the frame-so-far.
 249     descriptor_set_snapshot: vk::DescriptorSet,
 250     /// Mid-frame copy target for blur-behind plates: the swapchain content so
 251     /// far, sampled by the resumed pass's blur draws. Sized with the surface.
 252     snapshot_image: vk::Image,
 253     snapshot_view: vk::ImageView,
 254     snapshot_allocation: Option<Allocation>,
 255     backdrop_sampler: vk::Sampler,
 256     window_info: AllocatedBuffer,
 257     /// The bevel-profile generation `window_info` was last written with —
 258     /// `draw_frame_2d` rewrites the UBO when the layout global moves on.
 259     profile_gen: u64,
 260     /// The pinned relief heights (carve drop, roll rise) in physical px as
 261     /// last uploaded in WindowInfo — compared each frame, since editors set
 262     /// them straight into the style registry with no generation counter.
 263     relief_uploaded: (f32, f32),
 264     /// Same for the edge (roll) profile LUT.
 265     roll_profile_gen: u64,
 266     plate_features: AllocatedBuffer,
 267 
 268     frames: Vec<Frame>,
 269     frame_index: usize,
 270     text: TextStage,
 271     scene: SceneStage,
 272     image: ImageStage,
 273     /// Built lazily on the first `set_rt_scene`, so ordinary UI apps never
 274     /// compile the path-tracer pipeline.
 275     rt: Option<RtStage>,
 276 
 277     desired_extent: vk::Extent2D,
 278     corner_radius_px: f32,
 279     swapchain_dirty: bool,
 280     present_mode: vk::PresentModeKHR,
 281     present_debug_count: u64,
 282 
 283     // Declared last: everything above must be destroyed before the device/
 284     // instance the core tears down in its own Drop.
 285     core: super::core::VkCore,
 286 }
 287 
 288 /// Compile WGSL to SPIR-V. The Y-flip between wgpu NDC (Y-up) and Vulkan NDC
 289 /// (Y-down) is handled with a negative-height viewport (like wgpu-hal), NOT in
 290 /// the shader — flipping in the shader would reverse screen-space winding and
 291 /// break the 3D pipeline's back-face culling.
 292 pub(crate) fn compile_wgsl(source: &str) -> Vec<u32> {
 293     let module = naga::front::wgsl::parse_str(source).expect("WGSL parse failed");
 294     let info = naga::valid::Validator::new(
 295         naga::valid::ValidationFlags::all(),
 296         naga::valid::Capabilities::PUSH_CONSTANT,
 297     )
 298     .validate(&module)
 299     .expect("WGSL validation failed");
 300     let options = naga::back::spv::Options {
 301         lang_version: (1, 0),
 302         flags: naga::back::spv::WriterFlags::LABEL_VARYINGS,
 303         ..Default::default()
 304     };
 305     naga::back::spv::write_vec(&module, &info, &options, None).expect("SPIR-V write failed")
 306 }
 307 
 308 /// Cached SPIR-V for the always-compiled UI shaders. The daemon-style
 309 /// consumers (cce-cloud) build a renderer per popup; naga compilation is pure,
 310 /// so compile each shader once per process.
 311 pub(crate) fn shader2d_spirv() -> &'static [u32] {
 312     static SPIRV: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
 313     SPIRV.get_or_init(|| compile_wgsl(include_str!("shader2d.wgsl")))
 314 }
 315 
 316 pub(crate) fn glyph_spirv() -> &'static [u32] {
 317     static SPIRV: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
 318     SPIRV.get_or_init(|| compile_wgsl(include_str!("glyph.wgsl")))
 319 }
 320 
 321 pub(crate) fn scene3d_spirv() -> &'static [u32] {
 322     static SPIRV: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
 323     SPIRV.get_or_init(|| compile_wgsl(include_str!("scene3d.wgsl")))
 324 }
 325 
 326 /// Like [`compile_wgsl`], but with naga's RAY_QUERY capability and SPIR-V 1.4
 327 /// (required by SPV_KHR_ray_query). Only used on devices where the ray-query
 328 /// device stack was enabled — those are Vulkan 1.2+, which accepts 1.4.
 329 pub(crate) fn compile_wgsl_ray_query(source: &str) -> Vec<u32> {
 330     let module = naga::front::wgsl::parse_str(source).expect("WGSL parse failed");
 331     let info = naga::valid::Validator::new(
 332         naga::valid::ValidationFlags::all(),
 333         naga::valid::Capabilities::RAY_QUERY,
 334     )
 335     .validate(&module)
 336     .expect("WGSL validation failed");
 337     let options = naga::back::spv::Options {
 338         lang_version: (1, 4),
 339         flags: naga::back::spv::WriterFlags::LABEL_VARYINGS,
 340         ..Default::default()
 341     };
 342     naga::back::spv::write_vec(&module, &info, &options, None).expect("SPIR-V write failed")
 343 }
 344 
 345 const COLOR_RANGE: vk::ImageSubresourceRange = vk::ImageSubresourceRange {
 346     aspect_mask: vk::ImageAspectFlags::COLOR,
 347     base_mip_level: 0,
 348     level_count: 1,
 349     base_array_layer: 0,
 350     layer_count: 1,
 351 };
 352 
 353 /// One-time submit: clear a color image and leave it in SHADER_READ_ONLY, so a
 354 /// freshly created backdrop is always legal to sample.
 355 pub(crate) fn clear_image_to_shader_read(
 356     device: &ash::Device,
 357     queue: vk::Queue,
 358     command_pool: vk::CommandPool,
 359     image: vk::Image,
 360 ) {
 361     unsafe {
 362         let cmd = device
 363             .allocate_command_buffers(
 364                 &vk::CommandBufferAllocateInfo::default()
 365                     .command_pool(command_pool)
 366                     .level(vk::CommandBufferLevel::PRIMARY)
 367                     .command_buffer_count(1),
 368             )
 369             .expect("Failed to allocate init command buffer")[0];
 370         device
 371             .begin_command_buffer(
 372                 cmd,
 373                 &vk::CommandBufferBeginInfo::default()
 374                     .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
 375             )
 376             .unwrap();
 377         device.cmd_pipeline_barrier(
 378             cmd,
 379             vk::PipelineStageFlags::TOP_OF_PIPE,
 380             vk::PipelineStageFlags::TRANSFER,
 381             vk::DependencyFlags::empty(),
 382             &[],
 383             &[],
 384             &[vk::ImageMemoryBarrier::default()
 385                 .src_access_mask(vk::AccessFlags::empty())
 386                 .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
 387                 .old_layout(vk::ImageLayout::UNDEFINED)
 388                 .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
 389                 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
 390                 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
 391                 .image(image)
 392                 .subresource_range(COLOR_RANGE)],
 393         );
 394         device.cmd_clear_color_image(
 395             cmd,
 396             image,
 397             vk::ImageLayout::TRANSFER_DST_OPTIMAL,
 398             &vk::ClearColorValue { float32: [0.0, 0.0, 0.0, 0.0] },
 399             &[COLOR_RANGE],
 400         );
 401         device.cmd_pipeline_barrier(
 402             cmd,
 403             vk::PipelineStageFlags::TRANSFER,
 404             vk::PipelineStageFlags::FRAGMENT_SHADER,
 405             vk::DependencyFlags::empty(),
 406             &[],
 407             &[],
 408             &[vk::ImageMemoryBarrier::default()
 409                 .src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
 410                 .dst_access_mask(vk::AccessFlags::SHADER_READ)
 411                 .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
 412                 .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
 413                 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
 414                 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
 415                 .image(image)
 416                 .subresource_range(COLOR_RANGE)],
 417         );
 418         device.end_command_buffer(cmd).unwrap();
 419         let cmds = [cmd];
 420         let submit = vk::SubmitInfo::default().command_buffers(&cmds);
 421         device
 422             .queue_submit(queue, &[submit], vk::Fence::null())
 423             .expect("Init submit failed");
 424         device.queue_wait_idle(queue).expect("Init wait failed");
 425         device.free_command_buffers(command_pool, &cmds);
 426     }
 427 }
 428 
 429 /// The wgpu-convention viewport: Y flipped via negative height (Vulkan >= 1.1).
 430 pub(crate) fn flipped_viewport(extent: vk::Extent2D) -> vk::Viewport {
 431     vk::Viewport {
 432         x: 0.0,
 433         y: extent.height as f32,
 434         width: extent.width as f32,
 435         height: -(extent.height as f32),
 436         min_depth: 0.0,
 437         max_depth: 1.0,
 438     }
 439 }
 440 
 441 
 442 impl VkRenderer {
 443     /// # Safety
 444     /// `display_ptr` and `surface_ptr` must be live `wl_display` / `wl_surface`
 445     /// pointers that outlive the renderer (same contract as `WgpuAdapter::new`).
 446     pub unsafe fn new(
 447         display_ptr: *mut c_void,
 448         surface_ptr: *mut c_void,
 449         width: u32,
 450         height: u32,
 451         corner_radius_px: f32,
 452     ) -> Self {
 453         let t_new = std::time::Instant::now();
 454         let (mut core, surface) =
 455             super::core::VkCore::new_for_wayland_surface(display_ptr, surface_ptr);
 456         log::debug!("[timing] VkCore::new_for_wayland_surface: {:?}", t_new.elapsed());
 457         let t_rest = std::time::Instant::now();
 458         // Locals over the core for the setup below (methods use self.core.*).
 459         let device = core.device.clone();
 460         let queue = core.queue;
 461         let command_pool = core.command_pool;
 462         let physical_device = core.physical_device;
 463         let min_uniform_align = core.min_uniform_align;
 464         let surface_loader = core.surface_loader.clone();
 465         let allocator = core.allocator.as_mut().unwrap();
 466 
 467         // Surface format: prefer sRGB (wgpu's get_default_config sorts sRGB first,
 468         // so this matches the colors the app renders today).
 469         let formats = surface_loader
 470             .get_physical_device_surface_formats(physical_device, surface)
 471             .expect("No surface formats");
 472         let surface_format = formats
 473             .iter()
 474             .copied()
 475             .find(|f| {
 476                 (f.format == vk::Format::B8G8R8A8_SRGB || f.format == vk::Format::R8G8B8A8_SRGB)
 477                     && f.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR
 478             })
 479             .unwrap_or(formats[0]);
 480 
 481         // Render pass: one color attachment, clear -> present.
 482         let attachments = [vk::AttachmentDescription::default()
 483             .format(surface_format.format)
 484             .samples(vk::SampleCountFlags::TYPE_1)
 485             .load_op(vk::AttachmentLoadOp::CLEAR)
 486             .store_op(vk::AttachmentStoreOp::STORE)
 487             .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
 488             .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
 489             .initial_layout(vk::ImageLayout::UNDEFINED)
 490             .final_layout(vk::ImageLayout::PRESENT_SRC_KHR)];
 491         let color_refs = [vk::AttachmentReference::default()
 492             .attachment(0)
 493             .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)];
 494         let subpasses = [vk::SubpassDescription::default()
 495             .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
 496             .color_attachments(&color_refs)];
 497         // One dependency shared VERBATIM by both UI pass variants: framebuffer
 498         // compatibility requires identical dependencies (only load/store ops and
 499         // image layouts may differ), so this unions the clear case (previous
 500         // frame's color output) with the load case (the backdrop copy's write).
 501         let dependencies = [vk::SubpassDependency::default()
 502             .src_subpass(vk::SUBPASS_EXTERNAL)
 503             .dst_subpass(0)
 504             .src_stage_mask(
 505                 vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
 506                     | vk::PipelineStageFlags::TRANSFER,
 507             )
 508             .src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
 509             .dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
 510             .dst_access_mask(
 511                 vk::AccessFlags::COLOR_ATTACHMENT_READ | vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
 512             )];
 513         let render_pass = device
 514             .create_render_pass(
 515                 &vk::RenderPassCreateInfo::default()
 516                     .attachments(&attachments)
 517                     .subpasses(&subpasses)
 518                     .dependencies(&dependencies),
 519                 None,
 520             )
 521             .expect("Failed to create render pass");
 522 
 523         // Variant used when a backdrop copy precedes the UI pass: keep the copied
 524         // pixels (LOAD) and take the image from the copy's TRANSFER_DST layout.
 525         let attachments_load = [vk::AttachmentDescription::default()
 526             .format(surface_format.format)
 527             .samples(vk::SampleCountFlags::TYPE_1)
 528             .load_op(vk::AttachmentLoadOp::LOAD)
 529             .store_op(vk::AttachmentStoreOp::STORE)
 530             .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
 531             .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
 532             .initial_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
 533             .final_layout(vk::ImageLayout::PRESENT_SRC_KHR)];
 534         let render_pass_load = device
 535             .create_render_pass(
 536                 &vk::RenderPassCreateInfo::default()
 537                     .attachments(&attachments_load)
 538                     .subpasses(&subpasses)
 539                     .dependencies(&dependencies),
 540                 None,
 541             )
 542             .expect("Failed to create load render pass");
 543 
 544         // Descriptor set layout mirroring shader.wgsl @group(0): naga maps WGSL
 545         // texture/sampler/uniform bindings 1:1 onto set 0 descriptor bindings.
 546         let bindings = [
 547             vk::DescriptorSetLayoutBinding::default()
 548                 .binding(0)
 549                 .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
 550                 .descriptor_count(1)
 551                 .stage_flags(vk::ShaderStageFlags::FRAGMENT),
 552             vk::DescriptorSetLayoutBinding::default()
 553                 .binding(1)
 554                 .descriptor_type(vk::DescriptorType::SAMPLER)
 555                 .descriptor_count(1)
 556                 .stage_flags(vk::ShaderStageFlags::FRAGMENT),
 557             vk::DescriptorSetLayoutBinding::default()
 558                 .binding(2)
 559                 .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
 560                 .descriptor_count(1)
 561                 .stage_flags(vk::ShaderStageFlags::FRAGMENT),
 562             vk::DescriptorSetLayoutBinding::default()
 563                 .binding(3)
 564                 .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
 565                 .descriptor_count(1)
 566                 .stage_flags(vk::ShaderStageFlags::FRAGMENT),
 567         ];
 568         let descriptor_set_layout = device
 569             .create_descriptor_set_layout(
 570                 &vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
 571                 None,
 572             )
 573             .expect("Failed to create descriptor set layout");
 574 
 575         let set_layouts = [descriptor_set_layout];
 576         // Push constants: the per-batch rounded-rect clip plus the SDF-lit
 577         // plate block (eight vec4s, matching shader2d's `RRectClip`), read by
 578         // shader2d's fragment stage. See `PUSH_CONSTANT_BYTES` — the block is
 579         // exactly the Vulkan-guaranteed minimum and completely full.
 580         let push_ranges = [vk::PushConstantRange::default()
 581             .stage_flags(vk::ShaderStageFlags::FRAGMENT)
 582             .offset(0)
 583             .size(PUSH_CONSTANT_BYTES)];
 584         let pipeline_layout = device
 585             .create_pipeline_layout(
 586                 &vk::PipelineLayoutCreateInfo::default()
 587                     .set_layouts(&set_layouts)
 588                     .push_constant_ranges(&push_ranges),
 589                 None,
 590             )
 591             .expect("Failed to create pipeline layout");
 592 
 593         // Pipeline from shader.wgsl (both entry points live in one SPIR-V module).
 594         let spirv = shader2d_spirv();
 595         let shader_module = device
 596             .create_shader_module(&vk::ShaderModuleCreateInfo::default().code(spirv), None)
 597             .expect("Failed to create shader module");
 598 
 599         let stages = [
 600             vk::PipelineShaderStageCreateInfo::default()
 601                 .stage(vk::ShaderStageFlags::VERTEX)
 602                 .module(shader_module)
 603                 .name(c"vs_main"),
 604             vk::PipelineShaderStageCreateInfo::default()
 605                 .stage(vk::ShaderStageFlags::FRAGMENT)
 606                 .module(shader_module)
 607                 .name(c"fs_main"),
 608         ];
 609 
 610         // Vertex layout = cce_ui::engine::Vertex: pos vec2f, color vec4f, clip vec3f.
 611         let vertex_bindings = [vk::VertexInputBindingDescription::default()
 612             .binding(0)
 613             .stride(std::mem::size_of::<Vertex>() as u32)
 614             .input_rate(vk::VertexInputRate::VERTEX)];
 615         let vertex_attributes = [
 616             vk::VertexInputAttributeDescription::default()
 617                 .location(0)
 618                 .binding(0)
 619                 .format(vk::Format::R32G32_SFLOAT)
 620                 .offset(0),
 621             vk::VertexInputAttributeDescription::default()
 622                 .location(1)
 623                 .binding(0)
 624                 .format(vk::Format::R32G32B32A32_SFLOAT)
 625                 .offset(8),
 626             vk::VertexInputAttributeDescription::default()
 627                 .location(2)
 628                 .binding(0)
 629                 .format(vk::Format::R32G32B32_SFLOAT)
 630                 .offset(24),
 631         ];
 632         let vertex_input = vk::PipelineVertexInputStateCreateInfo::default()
 633             .vertex_binding_descriptions(&vertex_bindings)
 634             .vertex_attribute_descriptions(&vertex_attributes);
 635 
 636         let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::default()
 637             .topology(vk::PrimitiveTopology::TRIANGLE_LIST);
 638         let viewport_state = vk::PipelineViewportStateCreateInfo::default()
 639             .viewport_count(1)
 640             .scissor_count(1);
 641         let rasterization = vk::PipelineRasterizationStateCreateInfo::default()
 642             .polygon_mode(vk::PolygonMode::FILL)
 643             .cull_mode(vk::CullModeFlags::NONE)
 644             .front_face(vk::FrontFace::COUNTER_CLOCKWISE)
 645             .line_width(1.0);
 646         let multisample = vk::PipelineMultisampleStateCreateInfo::default()
 647             .rasterization_samples(vk::SampleCountFlags::TYPE_1);
 648         // wgpu::BlendState::ALPHA_BLENDING.
 649         let blend_attachments = [vk::PipelineColorBlendAttachmentState::default()
 650             .blend_enable(true)
 651             .src_color_blend_factor(vk::BlendFactor::SRC_ALPHA)
 652             .dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
 653             .color_blend_op(vk::BlendOp::ADD)
 654             .src_alpha_blend_factor(vk::BlendFactor::ONE)
 655             .dst_alpha_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
 656             .alpha_blend_op(vk::BlendOp::ADD)
 657             .color_write_mask(vk::ColorComponentFlags::RGBA)];
 658         let color_blend =
 659             vk::PipelineColorBlendStateCreateInfo::default().attachments(&blend_attachments);
 660         let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR];
 661         let dynamic_state =
 662             vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
 663 
 664         let pipeline = device
 665             .create_graphics_pipelines(
 666                 vk::PipelineCache::null(),
 667                 &[vk::GraphicsPipelineCreateInfo::default()
 668                     .stages(&stages)
 669                     .vertex_input_state(&vertex_input)
 670                     .input_assembly_state(&input_assembly)
 671                     .viewport_state(&viewport_state)
 672                     .rasterization_state(&rasterization)
 673                     .multisample_state(&multisample)
 674                     .color_blend_state(&color_blend)
 675                     .dynamic_state(&dynamic_state)
 676                     .layout(pipeline_layout)
 677                     .render_pass(render_pass)
 678                     .subpass(0)],
 679                 None,
 680             )
 681             .expect("Failed to create graphics pipeline")[0];
 682 
 683         // Full-size backdrop + depth live in the scene stage: the 3D pass renders
 684         // into the backdrop, and the UI pass samples it for blur-behind plates.
 685         let initial_extent = vk::Extent2D { width: width.max(1), height: height.max(1) };
 686         let scene = SceneStage::new(
 687             &device,
 688             allocator,
 689             surface_format.format,
 690             initial_extent,
 691             FRAMES_IN_FLIGHT,
 692             min_uniform_align,
 693             core.max_line_width,
 694         );
 695         clear_image_to_shader_read(&device, queue, command_pool, scene.backdrop_image);
 696 
 697         // Matches the wgpu backdrop sampler: linear, clamp-to-edge.
 698         let backdrop_sampler = device
 699             .create_sampler(
 700                 &vk::SamplerCreateInfo::default()
 701                     .mag_filter(vk::Filter::LINEAR)
 702                     .min_filter(vk::Filter::LINEAR)
 703                     .mipmap_mode(vk::SamplerMipmapMode::NEAREST)
 704                     .address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE)
 705                     .address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE)
 706                     .address_mode_w(vk::SamplerAddressMode::CLAMP_TO_EDGE),
 707                 None,
 708             )
 709             .expect("Failed to create sampler");
 710 
 711         let window_info = create_cpu_buffer(
 712             &device,
 713             allocator,
 714             WINDOW_INFO_BYTES,
 715             vk::BufferUsageFlags::UNIFORM_BUFFER,
 716             "window-info",
 717         );
 718         // Plate-carve features, one MAX_PLATE_FEATURES slot per frame in
 719         // flight so a write never races the previous frame's reads.
 720         let plate_features = create_cpu_buffer(
 721             &device,
 722             allocator,
 723             (FRAMES_IN_FLIGHT * MAX_PLATE_FEATURES * PLATE_FEATURE_BYTES) as vk::DeviceSize,
 724             vk::BufferUsageFlags::UNIFORM_BUFFER,
 725             "plate-features",
 726         );
 727 
 728         // Two sets: the scene-backdrop set and its snapshot twin (binding 0
 729         // differs; 1-3 alias the same sampler/uniforms).
 730         let pool_sizes = [
 731             vk::DescriptorPoolSize::default()
 732                 .ty(vk::DescriptorType::SAMPLED_IMAGE)
 733                 .descriptor_count(2),
 734             vk::DescriptorPoolSize::default()
 735                 .ty(vk::DescriptorType::SAMPLER)
 736                 .descriptor_count(2),
 737             vk::DescriptorPoolSize::default()
 738                 .ty(vk::DescriptorType::UNIFORM_BUFFER)
 739                 .descriptor_count(4),
 740         ];
 741         let descriptor_pool = device
 742             .create_descriptor_pool(
 743                 &vk::DescriptorPoolCreateInfo::default()
 744                     .max_sets(2)
 745                     .pool_sizes(&pool_sizes),
 746                 None,
 747             )
 748             .expect("Failed to create descriptor pool");
 749         let both_layouts = [descriptor_set_layout, descriptor_set_layout];
 750         let sets = device
 751             .allocate_descriptor_sets(
 752                 &vk::DescriptorSetAllocateInfo::default()
 753                     .descriptor_pool(descriptor_pool)
 754                     .set_layouts(&both_layouts),
 755             )
 756             .expect("Failed to allocate descriptor sets");
 757         let (descriptor_set, descriptor_set_snapshot) = (sets[0], sets[1]);
 758 
 759         let image_infos = [vk::DescriptorImageInfo::default()
 760             .image_view(scene.backdrop_view)
 761             .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)];
 762         let sampler_infos = [vk::DescriptorImageInfo::default().sampler(backdrop_sampler)];
 763         let buffer_infos = [vk::DescriptorBufferInfo::default()
 764             .buffer(window_info.buffer)
 765             .offset(0)
 766             .range(WINDOW_INFO_BYTES)];
 767         let feature_infos = [vk::DescriptorBufferInfo::default()
 768             .buffer(plate_features.buffer)
 769             .offset(0)
 770             .range((FRAMES_IN_FLIGHT * MAX_PLATE_FEATURES * PLATE_FEATURE_BYTES) as vk::DeviceSize)];
 771         device.update_descriptor_sets(
 772             &[
 773                 vk::WriteDescriptorSet::default()
 774                     .dst_set(descriptor_set)
 775                     .dst_binding(0)
 776                     .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
 777                     .image_info(&image_infos),
 778                 vk::WriteDescriptorSet::default()
 779                     .dst_set(descriptor_set)
 780                     .dst_binding(1)
 781                     .descriptor_type(vk::DescriptorType::SAMPLER)
 782                     .image_info(&sampler_infos),
 783                 vk::WriteDescriptorSet::default()
 784                     .dst_set(descriptor_set)
 785                     .dst_binding(2)
 786                     .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
 787                     .buffer_info(&buffer_infos),
 788                 vk::WriteDescriptorSet::default()
 789                     .dst_set(descriptor_set)
 790                     .dst_binding(3)
 791                     .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
 792                     .buffer_info(&feature_infos),
 793                 // Snapshot twin: bindings 1-3 alias the same objects; binding 0
 794                 // is written by `sync_backdrop_targets` once the snapshot image
 795                 // exists.
 796                 vk::WriteDescriptorSet::default()
 797                     .dst_set(descriptor_set_snapshot)
 798                     .dst_binding(1)
 799                     .descriptor_type(vk::DescriptorType::SAMPLER)
 800                     .image_info(&sampler_infos),
 801                 vk::WriteDescriptorSet::default()
 802                     .dst_set(descriptor_set_snapshot)
 803                     .dst_binding(2)
 804                     .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
 805                     .buffer_info(&buffer_infos),
 806                 vk::WriteDescriptorSet::default()
 807                     .dst_set(descriptor_set_snapshot)
 808                     .dst_binding(3)
 809                     .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
 810                     .buffer_info(&feature_infos),
 811             ],
 812             &[],
 813         );
 814 
 815         // Per-frame command buffers, sync, and vertex buffers.
 816         let cmds = device
 817             .allocate_command_buffers(
 818                 &vk::CommandBufferAllocateInfo::default()
 819                     .command_pool(command_pool)
 820                     .level(vk::CommandBufferLevel::PRIMARY)
 821                     .command_buffer_count(FRAMES_IN_FLIGHT as u32),
 822             )
 823             .expect("Failed to allocate command buffers");
 824         let frames = cmds
 825             .into_iter()
 826             .map(|cmd| Frame {
 827                 cmd,
 828                 image_available: device
 829                     .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)
 830                     .unwrap(),
 831                 in_flight: device
 832                     .create_fence(
 833                         &vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED),
 834                         None,
 835                     )
 836                     .unwrap(),
 837                 vertex: create_cpu_buffer(
 838                     &device,
 839                     allocator,
 840                     64 * 1024,
 841                     vk::BufferUsageFlags::VERTEX_BUFFER,
 842                     "vertices",
 843                 ),
 844                 vertex_count: 0,
 845                 overlay_start: 0,
 846                 overlay_count: 0,
 847             })
 848             .collect();
 849 
 850         let text = TextStage::new(&device, allocator, render_pass, FRAMES_IN_FLIGHT);
 851         let image = ImageStage::new(&device, allocator, render_pass, FRAMES_IN_FLIGHT);
 852 
 853         let swapchain_loader = ash::khr::swapchain::Device::new(&core.instance, &device);
 854         let mut renderer = Self {
 855             surface,
 856             swapchain_loader,
 857             swapchain: vk::SwapchainKHR::null(),
 858             swapchain_images: Vec::new(),
 859             surface_format,
 860             extent: vk::Extent2D { width: width.max(1), height: height.max(1) },
 861             swapchain_views: Vec::new(),
 862             framebuffers: Vec::new(),
 863             render_finished: Vec::new(),
 864             render_pass,
 865             render_pass_load,
 866             descriptor_set_layout,
 867             pipeline_layout,
 868             pipeline,
 869             shader_module,
 870             descriptor_pool,
 871             descriptor_set,
 872             descriptor_set_snapshot,
 873             snapshot_image: vk::Image::null(),
 874             snapshot_view: vk::ImageView::null(),
 875             snapshot_allocation: None,
 876             backdrop_sampler,
 877             window_info,
 878             profile_gen: 0,
 879             relief_uploaded: (0.0, 0.0),
 880             roll_profile_gen: 0,
 881             plate_features,
 882             frames,
 883             frame_index: 0,
 884             text,
 885             scene,
 886             image,
 887             rt: None,
 888             desired_extent: vk::Extent2D { width: width.max(1), height: height.max(1) },
 889             corner_radius_px,
 890             swapchain_dirty: false,
 891             present_mode: vk::PresentModeKHR::FIFO,
 892             present_debug_count: 0,
 893             core,
 894         };
 895         log::debug!("[timing] VkRenderer pipelines/stages: {:?}", t_rest.elapsed());
 896         let t_swap = std::time::Instant::now();
 897         renderer.create_swapchain();
 898         renderer.write_window_info();
 899         // The swapchain may have settled on a different extent than requested;
 900         // keep the backdrop targets in lockstep.
 901         renderer.sync_backdrop_targets();
 902         log::debug!("[timing] swapchain setup: {:?}", t_swap.elapsed());
 903         renderer
 904     }
 905 
 906     /// The window-clip corner radius as the shaders consume it: the nominal
 907     /// radius widened by the curvature-match factor, so the clip cuts along
 908     /// the same curve as window-scale plate corners (`plate_push_raised` with
 909     /// `scale_corners`) and a clipped window reads the same as a plate-drawn
 910     /// one. Capped at half the smaller extent, like the plate path's cap.
 911     fn clip_corner_radius(&self) -> f32 {
 912         let cap = 0.5 * self.extent.width.min(self.extent.height) as f32;
 913         (self.corner_radius_px * crate::layout::corner_span_factor()).min(cap)
 914     }
 915 
 916     /// The pinned relief heights in physical px, 0 = follow the width.
 917     fn relief_px(&self) -> (f32, f32) {
 918         let s = crate::scale::scale_factor().max(0.001);
 919         (
 920             crate::layout::bevel_height().map_or(0.0, |h| h * s),
 921             crate::layout::roll_height().map_or(0.0, |h| h * s),
 922         )
 923     }
 924 
 925     fn write_window_info(&mut self) {
 926         // [size/clip vec4][carve profile meta vec4][8 vec4 carve slopes]
 927         // [roll profile meta vec4][8 vec4 roll slopes][relief heights vec4]
 928         // — must stay in lockstep with shader2d's WindowInfo. (The frost
 929         // recipe is per plate, in its push block, since RFC material step 3.)
 930         let mut data = [0.0f32; WINDOW_INFO_BYTES as usize / 4];
 931         data[0] = self.extent.width as f32;
 932         data[1] = self.extent.height as f32;
 933         data[2] = self.clip_corner_radius();
 934         data[3] = crate::layout::corner_shape();
 935         if let Some(slopes) = crate::layout::bevel_profile_slopes() {
 936             data[4] = 1.0;
 937             data[5] = crate::layout::BEVEL_PROFILE_SAMPLES as f32;
 938             data[8..8 + slopes.len()].copy_from_slice(&slopes);
 939         }
 940         if let Some(slopes) = crate::layout::roll_profile_slopes() {
 941             data[40] = 1.0;
 942             data[41] = crate::layout::BEVEL_PROFILE_SAMPLES as f32;
 943             data[44..44 + slopes.len()].copy_from_slice(&slopes);
 944         }
 945         let relief = self.relief_px();
 946         data[76] = relief.0;
 947         data[77] = relief.1;
 948         self.relief_uploaded = relief;
 949         self.profile_gen = crate::layout::bevel_profile_generation();
 950         self.roll_profile_gen = crate::layout::roll_profile_generation();
 951         if let Some(allocation) = self.window_info.allocation.as_mut() {
 952             allocation.mapped_slice_mut().unwrap()[..WINDOW_INFO_BYTES as usize]
 953                 .copy_from_slice(bytemuck::cast_slice(&data));
 954         }
 955     }
 956 
 957     fn destroy_swapchain_resources(&mut self) {
 958         unsafe {
 959             for fb in self.framebuffers.drain(..) {
 960                 self.core.device.destroy_framebuffer(fb, None);
 961             }
 962             for view in self.swapchain_views.drain(..) {
 963                 self.core.device.destroy_image_view(view, None);
 964             }
 965             self.swapchain_images.clear();
 966             for sem in self.render_finished.drain(..) {
 967                 self.core.device.destroy_semaphore(sem, None);
 968             }
 969         }
 970     }
 971 
 972     fn create_swapchain(&mut self) {
 973         unsafe {
 974             let caps = self.core
 975                 .surface_loader
 976                 .get_physical_device_surface_capabilities(self.core.physical_device, self.surface)
 977                 .expect("Failed to query surface capabilities");
 978 
 979             // Wayland reports "extent defined by the swapchain" (u32::MAX); use the
 980             // size the configure events gave us.
 981             let extent = if caps.current_extent.width != u32::MAX {
 982                 caps.current_extent
 983             } else {
 984                 vk::Extent2D {
 985                     width: self
 986                         .desired_extent
 987                         .width
 988                         .clamp(caps.min_image_extent.width, caps.max_image_extent.width.max(1)),
 989                     height: self
 990                         .desired_extent
 991                         .height
 992                         .clamp(caps.min_image_extent.height, caps.max_image_extent.height.max(1)),
 993                 }
 994             };
 995 
 996             let mut image_count = caps.min_image_count + 1;
 997             if caps.max_image_count > 0 {
 998                 image_count = image_count.min(caps.max_image_count);
 999             }
1000 
1001             // Prefer premultiplied (what the DE's other clients pick), else opaque,
1002             // else whatever the surface offers.
1003             let composite_alpha = [
1004                 vk::CompositeAlphaFlagsKHR::PRE_MULTIPLIED,
1005                 vk::CompositeAlphaFlagsKHR::OPAQUE,
1006                 vk::CompositeAlphaFlagsKHR::POST_MULTIPLIED,
1007                 vk::CompositeAlphaFlagsKHR::INHERIT,
1008             ]
1009             .into_iter()
1010             .find(|&mode| caps.supported_composite_alpha.contains(mode))
1011             .unwrap_or(vk::CompositeAlphaFlagsKHR::OPAQUE);
1012 
1013             // MAILBOX when the driver offers it (Mesa Wayland always does):
1014             // FIFO's present throttle waits on the PREVIOUS present's frame
1015             // callback, and a surface the compositor never renders (off the
1016             // viewport) never gets one — the second-ever present then blocks
1017             // forever inside queue_present with the whole event loop behind
1018             // it. MAILBOX just replaces the queued buffer, so presenting to
1019             // an invisible surface is always safe. The demand-driven loop's
1020             // frame-callback gate keeps MAILBOX from free-running.
1021             let modes = self
1022                 .core
1023                 .surface_loader
1024                 .get_physical_device_surface_present_modes(self.core.physical_device, self.surface)
1025                 .unwrap_or_default();
1026             self.present_mode = if modes.contains(&vk::PresentModeKHR::MAILBOX) {
1027                 vk::PresentModeKHR::MAILBOX
1028             } else {
1029                 vk::PresentModeKHR::FIFO
1030             };
1031 
1032             let old_swapchain = self.swapchain;
1033             self.swapchain = self
1034                 .swapchain_loader
1035                 .create_swapchain(
1036                     &vk::SwapchainCreateInfoKHR::default()
1037                         .surface(self.surface)
1038                         .min_image_count(image_count)
1039                         .image_format(self.surface_format.format)
1040                         .image_color_space(self.surface_format.color_space)
1041                         .image_extent(extent)
1042                         .image_array_layers(1)
1043                         .image_usage(
1044                             vk::ImageUsageFlags::COLOR_ATTACHMENT
1045                                 | vk::ImageUsageFlags::TRANSFER_DST
1046                                 // Blur-behind plates copy the frame-so-far out
1047                                 // of the swapchain into the snapshot image.
1048                                 | vk::ImageUsageFlags::TRANSFER_SRC,
1049                         )
1050                         .image_sharing_mode(vk::SharingMode::EXCLUSIVE)
1051                         .pre_transform(caps.current_transform)
1052                         .composite_alpha(composite_alpha)
1053                         .present_mode(self.present_mode)
1054                         .clipped(true)
1055                         .old_swapchain(old_swapchain),
1056                     None,
1057                 )
1058                 .expect("Failed to create swapchain");
1059             if old_swapchain != vk::SwapchainKHR::null() {
1060                 self.swapchain_loader.destroy_swapchain(old_swapchain, None);
1061             }
1062             self.extent = extent;
1063             if extent.width == self.desired_extent.width && extent.height == self.desired_extent.height {
1064                 // Keep the two in step so a rebuild queued for a non-resize
1065                 // reason (suboptimal/out-of-date) doesn't hand
1066                 // `pending_extent` a stale or unclamped size.
1067                 self.desired_extent = extent;
1068             } else {
1069                 // The surface capabilities overrode the requested size (seen
1070                 // on suspend/resume, when caps briefly lag the real surface
1071                 // state). Presenting this swapchain would commit a buffer the
1072                 // caller never approved — paired with the wrong buffer scale
1073                 // that reads as a self-resize and half/double-sizes the
1074                 // window. Keep the request, requeue the rebuild, and let
1075                 // draw_frame skip the present until caps agree.
1076                 log::warn!(
1077                     "swapchain extent {}x{} != requested {}x{}; skipping present until they agree",
1078                     extent.width, extent.height,
1079                     self.desired_extent.width, self.desired_extent.height,
1080                 );
1081                 self.swapchain_dirty = true;
1082             }
1083 
1084             let images = self
1085                 .swapchain_loader
1086                 .get_swapchain_images(self.swapchain)
1087                 .expect("Failed to get swapchain images");
1088             self.swapchain_images = images.clone();
1089             let subresource_range = vk::ImageSubresourceRange::default()
1090                 .aspect_mask(vk::ImageAspectFlags::COLOR)
1091                 .base_mip_level(0)
1092                 .level_count(1)
1093                 .base_array_layer(0)
1094                 .layer_count(1);
1095             for image in &images {
1096                 let view = self.core
1097                     .device
1098                     .create_image_view(
1099                         &vk::ImageViewCreateInfo::default()
1100                             .image(*image)
1101                             .view_type(vk::ImageViewType::TYPE_2D)
1102                             .format(self.surface_format.format)
1103                             .subresource_range(subresource_range),
1104                         None,
1105                     )
1106                     .expect("Failed to create swapchain view");
1107                 self.swapchain_views.push(view);
1108                 let attachments = [view];
1109                 let fb = self.core
1110                     .device
1111                     .create_framebuffer(
1112                         &vk::FramebufferCreateInfo::default()
1113                             .render_pass(self.render_pass)
1114                             .attachments(&attachments)
1115                             .width(extent.width)
1116                             .height(extent.height)
1117                             .layers(1),
1118                         None,
1119                     )
1120                     .expect("Failed to create framebuffer");
1121                 self.framebuffers.push(fb);
1122                 self.render_finished.push(
1123                     self.core.device
1124                         .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)
1125                         .unwrap(),
1126                 );
1127             }
1128         }
1129     }
1130 
1131     fn recreate_swapchain(&mut self) {
1132         unsafe {
1133             let _ = self.core.device.device_wait_idle();
1134         }
1135         self.destroy_swapchain_resources();
1136         self.create_swapchain();
1137         self.write_window_info();
1138         self.sync_backdrop_targets();
1139     }
1140 
1141     /// Suspend the UI pass, copy the swapchain's frame-so-far into the blur
1142     /// snapshot image, and resume drawing — the mechanism behind blur-behind
1143     /// plates (`Batch2D::blur_behind`). Ending the pass leaves the swapchain in
1144     /// its PRESENT final layout; the copy walks it through TRANSFER_SRC and
1145     /// hands it back in TRANSFER_DST, which is exactly `render_pass_load`'s
1146     /// expected initial layout, so the resume reuses that pass (and the shared
1147     /// framebuffers). Dynamic viewport state dies with the pass and is restored;
1148     /// scissor/pipeline/descriptors are re-bound per draw by the batch loop.
1149     fn snapshot_frame_so_far(&self, cmd: vk::CommandBuffer, image_index: usize) {
1150         let device = &self.core.device;
1151         let swapchain_image = self.swapchain_images[image_index];
1152         unsafe {
1153             device.cmd_end_render_pass(cmd);
1154             device.cmd_pipeline_barrier(
1155                 cmd,
1156                 vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
1157                     | vk::PipelineStageFlags::FRAGMENT_SHADER,
1158                 vk::PipelineStageFlags::TRANSFER,
1159                 vk::DependencyFlags::empty(),
1160                 &[],
1161                 &[],
1162                 &[
1163                     vk::ImageMemoryBarrier::default()
1164                         .src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
1165                         .dst_access_mask(vk::AccessFlags::TRANSFER_READ)
1166                         .old_layout(vk::ImageLayout::PRESENT_SRC_KHR)
1167                         .new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
1168                         .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1169                         .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1170                         .image(swapchain_image)
1171                         .subresource_range(COLOR_RANGE),
1172                     // Covers the previous frame's blur reads of the snapshot.
1173                     vk::ImageMemoryBarrier::default()
1174                         .src_access_mask(vk::AccessFlags::SHADER_READ)
1175                         .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
1176                         .old_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
1177                         .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
1178                         .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1179                         .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1180                         .image(self.snapshot_image)
1181                         .subresource_range(COLOR_RANGE),
1182                 ],
1183             );
1184             let subresource = vk::ImageSubresourceLayers::default()
1185                 .aspect_mask(vk::ImageAspectFlags::COLOR)
1186                 .layer_count(1);
1187             device.cmd_copy_image(
1188                 cmd,
1189                 swapchain_image,
1190                 vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
1191                 self.snapshot_image,
1192                 vk::ImageLayout::TRANSFER_DST_OPTIMAL,
1193                 &[vk::ImageCopy::default()
1194                     .src_subresource(subresource)
1195                     .dst_subresource(subresource)
1196                     .extent(vk::Extent3D {
1197                         width: self.extent.width,
1198                         height: self.extent.height,
1199                         depth: 1,
1200                     })],
1201             );
1202             device.cmd_pipeline_barrier(
1203                 cmd,
1204                 vk::PipelineStageFlags::TRANSFER,
1205                 vk::PipelineStageFlags::FRAGMENT_SHADER | vk::PipelineStageFlags::TRANSFER,
1206                 vk::DependencyFlags::empty(),
1207                 &[],
1208                 &[],
1209                 &[
1210                     vk::ImageMemoryBarrier::default()
1211                         .src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
1212                         .dst_access_mask(vk::AccessFlags::SHADER_READ)
1213                         .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
1214                         .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
1215                         .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1216                         .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1217                         .image(self.snapshot_image)
1218                         .subresource_range(COLOR_RANGE),
1219                     vk::ImageMemoryBarrier::default()
1220                         .src_access_mask(vk::AccessFlags::TRANSFER_READ)
1221                         .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
1222                         .old_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
1223                         .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
1224                         .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1225                         .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1226                         .image(swapchain_image)
1227                         .subresource_range(COLOR_RANGE),
1228                 ],
1229             );
1230             device.cmd_begin_render_pass(
1231                 cmd,
1232                 &vk::RenderPassBeginInfo::default()
1233                     .render_pass(self.render_pass_load)
1234                     .framebuffer(self.framebuffers[image_index])
1235                     .render_area(vk::Rect2D {
1236                         offset: vk::Offset2D { x: 0, y: 0 },
1237                         extent: self.extent,
1238                     }),
1239                 vk::SubpassContents::INLINE,
1240             );
1241             device.cmd_set_viewport(cmd, 0, &[flipped_viewport(self.extent)]);
1242         }
1243     }
1244 
1245     /// Recreate backdrop + depth at the surface size (device must be idle),
1246     /// re-point the UI descriptor at the new view, and make the fresh image
1247     /// legal to sample.
1248     fn sync_backdrop_targets(&mut self) {
1249         self.scene.resize(
1250             &self.core.device,
1251             self.core.allocator.as_mut().unwrap(),
1252             self.extent,
1253         );
1254         clear_image_to_shader_read(
1255             &self.core.device,
1256             self.core.queue,
1257             self.core.command_pool,
1258             self.scene.backdrop_image,
1259         );
1260         // The blur snapshot target tracks the surface size alongside the
1261         // backdrop (same format so cmd_copy_image from the swapchain is legal).
1262         unsafe {
1263             let device = &self.core.device;
1264             if self.snapshot_view != vk::ImageView::null() {
1265                 device.destroy_image_view(self.snapshot_view, None);
1266                 device.destroy_image(self.snapshot_image, None);
1267                 self.snapshot_view = vk::ImageView::null();
1268                 self.snapshot_image = vk::Image::null();
1269             }
1270             if let Some(alloc) = self.snapshot_allocation.take() {
1271                 let _ = self.core.allocator.as_mut().unwrap().free(alloc);
1272             }
1273             let device = &self.core.device;
1274             let snapshot_image = device
1275                 .create_image(
1276                     &vk::ImageCreateInfo::default()
1277                         .image_type(vk::ImageType::TYPE_2D)
1278                         .format(self.surface_format.format)
1279                         .extent(vk::Extent3D {
1280                             width: self.extent.width,
1281                             height: self.extent.height,
1282                             depth: 1,
1283                         })
1284                         .mip_levels(1)
1285                         .array_layers(1)
1286                         .samples(vk::SampleCountFlags::TYPE_1)
1287                         .tiling(vk::ImageTiling::OPTIMAL)
1288                         .usage(
1289                             vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST,
1290                         )
1291                         .initial_layout(vk::ImageLayout::UNDEFINED),
1292                     None,
1293                 )
1294                 .expect("Failed to create snapshot image");
1295             let requirements = device.get_image_memory_requirements(snapshot_image);
1296             let allocation = self
1297                 .core
1298                 .allocator
1299                 .as_mut()
1300                 .unwrap()
1301                 .allocate(&AllocationCreateDesc {
1302                     name: "blur-snapshot",
1303                     requirements,
1304                     location: MemoryLocation::GpuOnly,
1305                     linear: false,
1306                     allocation_scheme: AllocationScheme::GpuAllocatorManaged,
1307                 })
1308                 .expect("Failed to allocate snapshot memory");
1309             self.core
1310                 .device
1311                 .bind_image_memory(snapshot_image, allocation.memory(), allocation.offset())
1312                 .expect("Failed to bind snapshot memory");
1313             let snapshot_view = self
1314                 .core
1315                 .device
1316                 .create_image_view(
1317                     &vk::ImageViewCreateInfo::default()
1318                         .image(snapshot_image)
1319                         .view_type(vk::ImageViewType::TYPE_2D)
1320                         .format(self.surface_format.format)
1321                         .subresource_range(COLOR_RANGE),
1322                     None,
1323                 )
1324                 .expect("Failed to create snapshot view");
1325             self.snapshot_image = snapshot_image;
1326             self.snapshot_view = snapshot_view;
1327             self.snapshot_allocation = Some(allocation);
1328         }
1329         // A fresh snapshot must be legal to sample before its first copy.
1330         clear_image_to_shader_read(
1331             &self.core.device,
1332             self.core.queue,
1333             self.core.command_pool,
1334             self.snapshot_image,
1335         );
1336         let image_infos = [vk::DescriptorImageInfo::default()
1337             .image_view(self.scene.backdrop_view)
1338             .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)];
1339         let snapshot_infos = [vk::DescriptorImageInfo::default()
1340             .image_view(self.snapshot_view)
1341             .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)];
1342         unsafe {
1343             self.core.device.update_descriptor_sets(
1344                 &[
1345                     vk::WriteDescriptorSet::default()
1346                         .dst_set(self.descriptor_set)
1347                         .dst_binding(0)
1348                         .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
1349                         .image_info(&image_infos),
1350                     vk::WriteDescriptorSet::default()
1351                         .dst_set(self.descriptor_set_snapshot)
1352                         .dst_binding(0)
1353                         .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
1354                         .image_info(&snapshot_infos),
1355                 ],
1356                 &[],
1357             );
1358         }
1359     }
1360 
1361     /// Upload a 3D mesh (Vertex3D: position + color); the id is stable for the
1362     /// renderer's lifetime.
1363     pub fn create_mesh(&mut self, verts: &[Vertex3D]) -> MeshId {
1364         self.scene
1365             .create_mesh(&self.core.device, self.core.allocator.as_mut().unwrap(), verts)
1366     }
1367 
1368     /// Replace a mesh's vertices. Waits for the GPU to go idle first — geometry
1369     /// updates are rare (settings changes, graph rebuilds), matching the app.
1370     #[allow(dead_code)] // cutover API: the app's rebuild_scene_geometry path
1371     pub fn update_mesh(&mut self, id: MeshId, verts: &[Vertex3D]) {
1372         unsafe {
1373             let _ = self.core.device.device_wait_idle();
1374         }
1375         self.scene
1376             .update_mesh(&self.core.device, self.core.allocator.as_mut().unwrap(), id, verts);
1377     }
1378 
1379     /// Stage the 3D scene for the next `draw_frame`. Draws render into the
1380     /// backdrop image (scissored to the viewport pane, physical pixels), which
1381     /// is copied beneath the UI and doubles as the blur-behind source. Frames
1382     /// with no staged scene reuse the previous backdrop — the ash equivalent of
1383     /// the app's viewport-changed cache.
1384     pub fn stage_scene(&mut self, scissor: (u32, u32, u32, u32), draws: Vec<SceneDraw>) {
1385         self.scene.stage(scissor, draws);
1386     }
1387 
1388     /// Replace the path tracer's scene (triangles in the space the camera's
1389     /// `inv_mvp` unprojects into). Builds the BVH on the CPU and uploads it;
1390     /// waits for the GPU to go idle first — scene replacement is rare
1391     /// (geometry rebuilds), matching `update_mesh`. The first call compiles
1392     /// the compute pipeline.
1393     pub fn set_rt_scene(&mut self, triangles: &[RtTriangle], materials: &[RtMaterial]) {
1394         unsafe {
1395             let _ = self.core.device.device_wait_idle();
1396         }
1397         let core = &mut self.core;
1398         let allocator = core.allocator.as_mut().unwrap();
1399         let rt = self.rt.get_or_insert_with(|| {
1400             RtStage::new(
1401                 &core.device,
1402                 allocator,
1403                 FRAMES_IN_FLIGHT,
1404                 core.accel_loader.as_ref(),
1405                 core.as_scratch_align,
1406                 core.min_uniform_align,
1407             )
1408         });
1409         rt.set_scene(
1410             &core.device,
1411             allocator,
1412             core.queue,
1413             core.command_pool,
1414             triangles,
1415             materials,
1416         );
1417     }
1418 
1419     /// Stage one progressive path-tracing pass into the viewport pane
1420     /// (physical pixels) for the next `draw_frame`. Call it every frame while
1421     /// RT mode is on: each frame adds a sample; a camera/pane/scene change
1422     /// restarts the accumulation. No-op until `set_rt_scene` has run.
1423     pub fn stage_rt(&mut self, pane: (u32, u32, u32, u32), camera: RtCamera) {
1424         if let Some(rt) = self.rt.as_mut() {
1425             rt.stage(
1426                 &self.core.device,
1427                 self.core.allocator.as_mut().unwrap(),
1428                 pane,
1429                 camera,
1430             );
1431         }
1432     }
1433 
1434     /// True while more `stage_rt` + `draw_frame` rounds would still refine the
1435     /// image — the app's cue to keep requesting frames.
1436     pub fn rt_accumulating(&self) -> bool {
1437         self.rt.as_ref().is_some_and(|rt| rt.accumulating())
1438     }
1439 
1440     /// Whether presenting past an unacknowledged frame callback is safe.
1441     /// True under MAILBOX (the present replaces the queued buffer). Under
1442     /// FIFO the driver's present throttle waits on the previous present's
1443     /// frame event, so a forced present to a surface the compositor isn't
1444     /// rendering blocks forever — the caller must not force one.
1445     pub fn forced_present_safe(&self) -> bool {
1446         self.present_mode == vk::PresentModeKHR::MAILBOX
1447     }
1448 
1449     /// The extent the next `draw_frame` will render at: the pending size when a
1450     /// swapchain rebuild is queued, otherwise the live one.
1451     pub fn pending_extent(&self) -> vk::Extent2D {
1452         if self.swapchain_dirty { self.desired_extent } else { self.extent }
1453     }
1454 
1455     /// Request a new physical size (from xdg configure / scale changes). Applied
1456     /// lazily on the next `draw_frame`.
1457     pub fn resize(&mut self, width: u32, height: u32) {
1458         let extent = vk::Extent2D { width: width.max(1), height: height.max(1) };
1459         // Record the request unconditionally, not just when it differs from the
1460         // live extent: with a rebuild already queued (`swapchain_dirty`), a
1461         // request that returns to the live size must overwrite the queued one.
1462         // Otherwise `desired_extent` stays wedged at the intermediate size, the
1463         // caller's `pending_extent` gate never matches, and no frame presents
1464         // again — a resume scale bounce (2→1→2 before any draw) froze the
1465         // status-bar clock exactly this way.
1466         self.desired_extent = extent;
1467         if extent.width != self.extent.width || extent.height != self.extent.height {
1468             self.swapchain_dirty = true;
1469         }
1470     }
1471 
1472     // Used at cutover, when scale changes re-derive the radius; vk-smoke fixes it at init.
1473     // Nominal (circle-equivalent) radius in physical px — the curvature-match
1474     // widening for squircle corner shapes happens at consumption
1475     // (`clip_corner_radius`), so callers pass the configured radius as-is.
1476     #[allow(dead_code)]
1477     pub fn set_corner_radius(&mut self, radius_px: f32) {
1478         self.corner_radius_px = radius_px;
1479         // Written on the next swapchain rebuild or draw-idle moment; a mapped write
1480         // here would race in-flight frames, so route it through the dirty path.
1481         self.swapchain_dirty = true;
1482     }
1483 
1484     /// Stage text for the next `draw_frame`: shape-cache misses are rasterized
1485     /// into the glyph atlas and vertices are built against the current extent.
1486     /// Mirrors what was `glyphon::TextRenderer::prepare`.
1487     pub fn prepare_text(
1488         &mut self,
1489         font_system: &mut cosmic_text::FontSystem,
1490         swash_cache: &mut cosmic_text::SwashCache,
1491         spans: &[TextSpan<'_>],
1492     ) {
1493         // Against the extent this frame will actually be drawn at: `resize` is
1494         // lazy, so with a rebuild pending `self.extent` is still the previous
1495         // size and text would land in the wrong NDC (visibly mis-scaled and
1496         // offset while a window auto-sizes to its content).
1497         self.text.prepare(font_system, swash_cache, spans, self.pending_extent());
1498     }
1499 
1500     /// Render one frame of plain 2D geometry: a single unclipped batch, no
1501     /// overlay, transparent clear. See [`VkRenderer::draw_frame_2d`].
1502     pub fn draw_frame(&mut self, verts: &[Vertex]) -> bool {
1503         self.draw_frame_2d(Frame2D {
1504             verts,
1505             batches: &[],
1506             overlay_verts: &[],
1507             images: &[],
1508             plate_features: &[],
1509             clear_color: [0.0; 4],
1510         })
1511     }
1512 
1513     /// Render one frame: the 2D geometry (optionally as scissored batches),
1514     /// then any text staged via `prepare_text`, then the overlay vertices on
1515     /// top. Returns false if the frame was skipped (swapchain rebuild); the
1516     /// caller just draws again next tick.
1517     pub fn draw_frame_2d(&mut self, frame2d: Frame2D<'_>) -> bool {
1518         if self.swapchain_dirty {
1519             self.swapchain_dirty = false;
1520             self.recreate_swapchain();
1521             if self.swapchain_dirty {
1522                 // The rebuild couldn't honor the requested extent (surface
1523                 // caps disagree, e.g. mid suspend/resume) — presenting it
1524                 // would commit a wrong-size buffer. Skip; the caller redraws.
1525                 return false;
1526             }
1527         }
1528 
1529         unsafe {
1530             let frame_index = self.frame_index;
1531             let (in_flight, image_available) = {
1532                 let f = &self.frames[frame_index];
1533                 (f.in_flight, f.image_available)
1534             };
1535             self.core.device
1536                 .wait_for_fences(&[in_flight], true, u64::MAX)
1537                 .expect("Fence wait failed");
1538 
1539             if present_debug() {
1540                 eprintln!("[vk] frame {} acquire...", self.present_debug_count);
1541             }
1542             let image_index = match self.swapchain_loader.acquire_next_image(
1543                 self.swapchain,
1544                 u64::MAX,
1545                 image_available,
1546                 vk::Fence::null(),
1547             ) {
1548                 Ok((index, suboptimal)) => {
1549                     if suboptimal {
1550                         self.swapchain_dirty = true;
1551                     }
1552                     index
1553                 }
1554                 Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
1555                     self.swapchain_dirty = true;
1556                     return false;
1557                 }
1558                 Err(e) => {
1559                     log::error!("acquire_next_image failed: {e:?}");
1560                     return false;
1561                 }
1562             };
1563 
1564             self.core.device.reset_fences(&[in_flight]).unwrap();
1565 
1566             // Re-upload the bevel-profile LUT when it changed (a live ramp
1567             // edit). The other in-flight frame may still read the old bytes —
1568             // both are valid profiles, so the one-frame mix is benign.
1569             if self.profile_gen != crate::layout::bevel_profile_generation()
1570                 || self.roll_profile_gen != crate::layout::roll_profile_generation()
1571                 || self.relief_uploaded != self.relief_px()
1572             {
1573                 self.write_window_info();
1574             }
1575 
1576             // Upload this frame's plate carves into its slot of the feature
1577             // UBO (the slot's previous user has fenced, so no race).
1578             if !frame2d.plate_features.is_empty() {
1579                 let n = frame2d.plate_features.len().min(MAX_PLATE_FEATURES);
1580                 let base = frame_index * MAX_PLATE_FEATURES * PLATE_FEATURE_BYTES;
1581                 if let Some(allocation) = self.plate_features.allocation.as_mut() {
1582                     let bytes: &[u8] = bytemuck::cast_slice(&frame2d.plate_features[..n]);
1583                     allocation.mapped_slice_mut().unwrap()[base..base + bytes.len()]
1584                         .copy_from_slice(bytes);
1585                 }
1586             }
1587 
1588             // Upload display-list + overlay vertices into this frame's buffer
1589             // (its fence has signaled, so the GPU is done with it; growing swaps
1590             // in a fresh buffer). Overlay verts sit after the main range.
1591             let vert_bytes: &[u8] = bytemuck::cast_slice(frame2d.verts);
1592             let overlay_bytes: &[u8] = bytemuck::cast_slice(frame2d.overlay_verts);
1593             let needed = (vert_bytes.len() + overlay_bytes.len()) as vk::DeviceSize;
1594             if needed > self.frames[frame_index].vertex.size {
1595                 let mut old =
1596                     std::mem::replace(&mut self.frames[frame_index].vertex, AllocatedBuffer::null());
1597                 let allocator = self.core.allocator.as_mut().unwrap();
1598                 destroy_cpu_buffer(&self.core.device, allocator, &mut old);
1599                 self.frames[frame_index].vertex = create_cpu_buffer(
1600                     &self.core.device,
1601                     allocator,
1602                     needed.next_power_of_two(),
1603                     vk::BufferUsageFlags::VERTEX_BUFFER,
1604                     "vertices",
1605                 );
1606             }
1607             if needed > 0 {
1608                 let mapped = self.frames[frame_index]
1609                     .vertex
1610                     .allocation
1611                     .as_mut()
1612                     .unwrap()
1613                     .mapped_slice_mut()
1614                     .unwrap();
1615                 mapped[..vert_bytes.len()].copy_from_slice(vert_bytes);
1616                 mapped[vert_bytes.len()..vert_bytes.len() + overlay_bytes.len()]
1617                     .copy_from_slice(overlay_bytes);
1618             }
1619             self.frames[frame_index].vertex_count = frame2d.verts.len() as u32;
1620             self.frames[frame_index].overlay_start = frame2d.verts.len() as u32;
1621             self.frames[frame_index].overlay_count = frame2d.overlay_verts.len() as u32;
1622             self.text.write_frame_buffers(
1623                 &self.core.device,
1624                 self.core.allocator.as_mut().unwrap(),
1625                 frame_index,
1626             );
1627             self.image.process_pending(
1628                 &self.core.device,
1629                 self.core.allocator.as_mut().unwrap(),
1630                 self.core.queue,
1631                 self.core.command_pool,
1632             );
1633             self.image.write_frame_buffer(
1634                 &self.core.device,
1635                 self.core.allocator.as_mut().unwrap(),
1636                 frame_index,
1637                 frame2d.images,
1638                 self.extent,
1639             );
1640             let clip_radius = self.clip_corner_radius();
1641             self.scene.write_frame_uniforms(
1642                 &self.core.device,
1643                 self.core.allocator.as_mut().unwrap(),
1644                 frame_index,
1645                 clip_radius,
1646             );
1647             if let Some(rt) = self.rt.as_mut() {
1648                 rt.write_frame_uniforms(frame_index);
1649             }
1650 
1651             // Record.
1652             let cmd = self.frames[frame_index].cmd;
1653             self.core.device
1654                 .begin_command_buffer(cmd, &vk::CommandBufferBeginInfo::default())
1655                 .unwrap();
1656             self.text.record_upload(&self.core.device, cmd, frame_index);
1657 
1658             // Offscreen 3D pass (only when a scene was staged); leaves the
1659             // backdrop in TRANSFER_SRC.
1660             let mut scene_recorded = self.scene.record(&self.core.device, cmd, frame_index);
1661 
1662             // Path-tracer pass (only when staged via `stage_rt`): one
1663             // accumulation dispatch, blitted into the backdrop's pane region —
1664             // it fills the same slot as the raster scene pass and leaves the
1665             // backdrop in TRANSFER_SRC likewise.
1666             if let Some(rt) = self.rt.as_mut() {
1667                 let rt_recorded = rt.record(
1668                     &self.core.device,
1669                     cmd,
1670                     frame_index,
1671                     self.scene.backdrop_image,
1672                     self.extent,
1673                     scene_recorded,
1674                 );
1675                 if rt_recorded {
1676                     self.scene.backdrop_valid = true;
1677                     scene_recorded = true;
1678                 }
1679             }
1680 
1681             // With a valid backdrop, replay it under the UI: copy it into the
1682             // swapchain image and open the UI pass with LOAD instead of CLEAR.
1683             let use_backdrop = self.scene.backdrop_valid;
1684             if use_backdrop {
1685                 if !scene_recorded {
1686                     // Reused backdrop is in SHADER_READ_ONLY from last frame.
1687                     self.core.device.cmd_pipeline_barrier(
1688                         cmd,
1689                         vk::PipelineStageFlags::FRAGMENT_SHADER,
1690                         vk::PipelineStageFlags::TRANSFER,
1691                         vk::DependencyFlags::empty(),
1692                         &[],
1693                         &[],
1694                         &[vk::ImageMemoryBarrier::default()
1695                             .src_access_mask(vk::AccessFlags::SHADER_READ)
1696                             .dst_access_mask(vk::AccessFlags::TRANSFER_READ)
1697                             .old_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
1698                             .new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
1699                             .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1700                             .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1701                             .image(self.scene.backdrop_image)
1702                             .subresource_range(COLOR_RANGE)],
1703                     );
1704                 }
1705                 let swapchain_image = self.swapchain_images[image_index as usize];
1706                 self.core.device.cmd_pipeline_barrier(
1707                     cmd,
1708                     vk::PipelineStageFlags::TOP_OF_PIPE,
1709                     vk::PipelineStageFlags::TRANSFER,
1710                     vk::DependencyFlags::empty(),
1711                     &[],
1712                     &[],
1713                     &[vk::ImageMemoryBarrier::default()
1714                         .src_access_mask(vk::AccessFlags::empty())
1715                         .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
1716                         .old_layout(vk::ImageLayout::UNDEFINED)
1717                         .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
1718                         .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1719                         .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1720                         .image(swapchain_image)
1721                         .subresource_range(COLOR_RANGE)],
1722                 );
1723                 let subresource = vk::ImageSubresourceLayers::default()
1724                     .aspect_mask(vk::ImageAspectFlags::COLOR)
1725                     .layer_count(1);
1726                 self.core.device.cmd_copy_image(
1727                     cmd,
1728                     self.scene.backdrop_image,
1729                     vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
1730                     swapchain_image,
1731                     vk::ImageLayout::TRANSFER_DST_OPTIMAL,
1732                     &[vk::ImageCopy::default()
1733                         .src_subresource(subresource)
1734                         .dst_subresource(subresource)
1735                         .extent(vk::Extent3D {
1736                             width: self.extent.width,
1737                             height: self.extent.height,
1738                             depth: 1,
1739                         })],
1740                 );
1741                 // Backdrop back to sampleable for the UI pass's blur plates.
1742                 self.core.device.cmd_pipeline_barrier(
1743                     cmd,
1744                     vk::PipelineStageFlags::TRANSFER,
1745                     vk::PipelineStageFlags::FRAGMENT_SHADER,
1746                     vk::DependencyFlags::empty(),
1747                     &[],
1748                     &[],
1749                     &[vk::ImageMemoryBarrier::default()
1750                         .src_access_mask(vk::AccessFlags::TRANSFER_READ)
1751                         .dst_access_mask(vk::AccessFlags::SHADER_READ)
1752                         .old_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
1753                         .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
1754                         .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1755                         .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1756                         .image(self.scene.backdrop_image)
1757                         .subresource_range(COLOR_RANGE)],
1758                 );
1759             }
1760 
1761             let frame = &self.frames[frame_index];
1762             let clear_values = [vk::ClearValue {
1763                 color: vk::ClearColorValue { float32: frame2d.clear_color },
1764             }];
1765             let (ui_pass, ui_clear_values): (vk::RenderPass, &[vk::ClearValue]) = if use_backdrop {
1766                 (self.render_pass_load, &[])
1767             } else {
1768                 (self.render_pass, &clear_values)
1769             };
1770             self.core.device.cmd_begin_render_pass(
1771                 cmd,
1772                 &vk::RenderPassBeginInfo::default()
1773                     .render_pass(ui_pass)
1774                     .framebuffer(self.framebuffers[image_index as usize])
1775                     .render_area(vk::Rect2D {
1776                         offset: vk::Offset2D { x: 0, y: 0 },
1777                         extent: self.extent,
1778                     })
1779                     .clear_values(ui_clear_values),
1780                 vk::SubpassContents::INLINE,
1781             );
1782             self.core.device
1783                 .cmd_set_viewport(cmd, 0, &[flipped_viewport(self.extent)]);
1784             self.core.device.cmd_set_scissor(
1785                 cmd,
1786                 0,
1787                 &[vk::Rect2D {
1788                     offset: vk::Offset2D { x: 0, y: 0 },
1789                     extent: self.extent,
1790                 }],
1791             );
1792             let full_scissor = vk::Rect2D {
1793                 offset: vk::Offset2D { x: 0, y: 0 },
1794                 extent: self.extent,
1795             };
1796             // Display-list geometry interleaved with user images: each image
1797             // quad draws before the vertex its `z_before` names, so it sits
1798             // above earlier geometry and below later geometry.
1799             {
1800                 let images = frame2d.images;
1801                 let mut order: Vec<usize> = (0..images.len()).collect();
1802                 order.sort_by_key(|&k| images[k].z_before);
1803                 let mut img_i = 0usize;
1804 
1805                 // Corner-shape exponent for the rounded-rect clip SDF, so clipped
1806                 // edges cut along the same squircle family as the tessellated and
1807                 // SDF-lit plate corners (a plate batch overwrites the slot with
1808                 // its own — identical — per-plate value).
1809                 let clip_shape = crate::layout::corner_shape();
1810 
1811                 let default_batch = [Batch2D {
1812                     scissor: None,
1813                     clip_rrect: None,
1814                     start: 0,
1815                     end: frame.vertex_count,
1816                     plate: None,
1817                     blur_behind: false,
1818                 }];
1819                 let batches: &[Batch2D] =
1820                     if frame2d.batches.is_empty() { &default_batch } else { frame2d.batches };
1821 
1822                 // Which @group(0) the vertex draws bind: the scene-backdrop set
1823                 // until the first blur-behind snapshot, the snapshot set after —
1824                 // so blur plates sample the frame-so-far, and later blur plates
1825                 // sample refreshed copies that include earlier ones.
1826                 let mut active_set = self.descriptor_set;
1827                 // CONSECUTIVE blur plates share one snapshot: only a non-blur
1828                 // draw invalidates it. A run of blur plates (the designer's
1829                 // node bodies) costs one copy, not one per plate — they don't
1830                 // see each other, which only matters where they overlap.
1831                 let mut snapshot_fresh = false;
1832 
1833                 for batch in batches {
1834                     // Images due at this batch's boundary draw first: they sit
1835                     // beneath the batch's geometry, and a blur snapshot taken
1836                     // for this batch must capture them (an image whose
1837                     // `z_before` equals the batch start would otherwise slip
1838                     // to after the snapshot and never be frosted).
1839                     while let Some(&k) = order.get(img_i) {
1840                         let q = &images[k];
1841                         if q.z_before > batch.start {
1842                             break;
1843                         }
1844                         img_i += 1;
1845                         let img_scissor = match q.clip {
1846                             Some((cx, cy, cw, ch)) => vk::Rect2D {
1847                                 offset: vk::Offset2D { x: cx as i32, y: cy as i32 },
1848                                 extent: vk::Extent2D {
1849                                     width: cw.min(self.extent.width.saturating_sub(cx)),
1850                                     height: ch.min(self.extent.height.saturating_sub(cy)),
1851                                 },
1852                             },
1853                             None => full_scissor,
1854                         };
1855                         self.core.device.cmd_set_scissor(cmd, 0, &[img_scissor]);
1856                         self.image.record_quad(&self.core.device, cmd, frame_index, k, q.image);
1857                         snapshot_fresh = false;
1858                     }
1859                     if batch.blur_behind {
1860                         if !snapshot_fresh {
1861                             self.snapshot_frame_so_far(cmd, image_index as usize);
1862                             active_set = self.descriptor_set_snapshot;
1863                             snapshot_fresh = true;
1864                         }
1865                     } else if batch.start < batch.end {
1866                         snapshot_fresh = false;
1867                     }
1868                     // Resolve the batch scissor; a degenerate one skips the
1869                     // vertex draws (images still process on their own clips).
1870                     let batch_scissor: Option<vk::Rect2D> = match batch.scissor {
1871                         Some((bx, by, bw, bh)) => {
1872                             if bx >= self.extent.width || by >= self.extent.height {
1873                                 None
1874                             } else {
1875                                 let bw = bw.min(self.extent.width - bx);
1876                                 let bh = bh.min(self.extent.height - by);
1877                                 if bw == 0 || bh == 0 {
1878                                     None
1879                                 } else {
1880                                     Some(vk::Rect2D {
1881                                         offset: vk::Offset2D { x: bx as i32, y: by as i32 },
1882                                         extent: vk::Extent2D { width: bw, height: bh },
1883                                     })
1884                                 }
1885                             }
1886                         }
1887                         None => Some(full_scissor),
1888                     };
1889 
1890                     let mut cursor = batch.start;
1891                     while cursor < batch.end {
1892                         let next_z =
1893                             order.get(img_i).map(|&k| images[k].z_before).unwrap_or(u32::MAX);
1894                         if next_z <= cursor {
1895                             let k = order[img_i];
1896                             img_i += 1;
1897                             let q = &images[k];
1898                             let img_scissor = match q.clip {
1899                                 Some((cx, cy, cw, ch)) => vk::Rect2D {
1900                                     offset: vk::Offset2D { x: cx as i32, y: cy as i32 },
1901                                     extent: vk::Extent2D {
1902                                         width: cw.min(self.extent.width.saturating_sub(cx)),
1903                                         height: ch.min(self.extent.height.saturating_sub(cy)),
1904                                     },
1905                                 },
1906                                 None => full_scissor,
1907                             };
1908                             self.core.device.cmd_set_scissor(cmd, 0, &[img_scissor]);
1909                             self.image.record_quad(&self.core.device, cmd, frame_index, k, q.image);
1910                             continue;
1911                         }
1912                         let upto = next_z.min(batch.end);
1913                         if let Some(scissor) = batch_scissor {
1914                             self.core.device
1915                                 .cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline);
1916                             self.core.device.cmd_bind_descriptor_sets(
1917                                 cmd,
1918                                 vk::PipelineBindPoint::GRAPHICS,
1919                                 self.pipeline_layout,
1920                                 0,
1921                                 &[active_set],
1922                                 &[],
1923                             );
1924                             self.core.device
1925                                 .cmd_bind_vertex_buffers(cmd, 0, &[frame.vertex.buffer], &[0]);
1926                             self.core.device.cmd_set_scissor(cmd, 0, &[scissor]);
1927                             // Per-batch rounded-rect clip (fragments outside
1928                             // discard) + the SDF-lit plate block when this
1929                             // batch is a plate cover quad.
1930                             let rr = batch.clip_rrect.unwrap_or([0.0; 5]);
1931                             let enabled = if batch.clip_rrect.is_some() { 1.0f32 } else { 0.0 };
1932                             let mut pc = [0.0f32; PUSH_CONSTANT_FLOATS];
1933                             pc[..5].copy_from_slice(&rr);
1934                             pc[5] = enabled;
1935                             pc[7] = clip_shape;
1936                             if let Some(p) = &batch.plate {
1937                                 pc[6] = p.mode;
1938                                 pc[7] = p.shape;
1939                                 pc[8..12].copy_from_slice(&p.rect);
1940                                 pc[12..16].copy_from_slice(&p.radii);
1941                                 pc[16..20].copy_from_slice(&p.light);
1942                                 pc[20..24].copy_from_slice(&p.material);
1943                                 pc[24..28].copy_from_slice(&p.host);
1944                                 pc[28..32].copy_from_slice(&p.specular_tint);
1945                                 if p.mode == 1.0 || p.mode == 14.0 {
1946                                     // Rebase the feature offset onto this
1947                                     // frame's UBO slot (a plate's CSG carves,
1948                                     // or a union carve's boxes).
1949                                     pc[24] += (frame_index * MAX_PLATE_FEATURES) as f32;
1950                                 }
1951                             }
1952                             self.core.device.cmd_push_constants(
1953                                 cmd,
1954                                 self.pipeline_layout,
1955                                 vk::ShaderStageFlags::FRAGMENT,
1956                                 0,
1957                                 bytemuck::cast_slice(&pc),
1958                             );
1959                             self.core.device.cmd_draw(cmd, upto - cursor, 1, cursor, 0);
1960                         }
1961                         cursor = upto;
1962                     }
1963                 }
1964                 // Images sorting after all geometry.
1965                 while let Some(&k) = order.get(img_i) {
1966                     img_i += 1;
1967                     let q = &images[k];
1968                     let img_scissor = match q.clip {
1969                         Some((cx, cy, cw, ch)) => vk::Rect2D {
1970                             offset: vk::Offset2D { x: cx as i32, y: cy as i32 },
1971                             extent: vk::Extent2D {
1972                                 width: cw.min(self.extent.width.saturating_sub(cx)),
1973                                 height: ch.min(self.extent.height.saturating_sub(cy)),
1974                             },
1975                         },
1976                         None => full_scissor,
1977                     };
1978                     self.core.device.cmd_set_scissor(cmd, 0, &[img_scissor]);
1979                     self.image.record_quad(&self.core.device, cmd, frame_index, k, q.image);
1980                 }
1981                 // Restore for the text/overlay draws.
1982                 self.core.device.cmd_set_scissor(cmd, 0, &[full_scissor]);
1983             }
1984             self.text.record_draw(&self.core.device, cmd, frame_index);
1985             if frame.overlay_count > 0 {
1986                 // The text pass bound its own pipeline; rebind for the overlay.
1987                 self.core.device
1988                     .cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline);
1989                 self.core.device.cmd_bind_descriptor_sets(
1990                     cmd,
1991                     vk::PipelineBindPoint::GRAPHICS,
1992                     self.pipeline_layout,
1993                     0,
1994                     &[self.descriptor_set],
1995                     &[],
1996                 );
1997                 self.core.device
1998                     .cmd_bind_vertex_buffers(cmd, 0, &[frame.vertex.buffer], &[0]);
1999                 // Push constants persist across binds — clear any batch's
2000                 // rounded clip and plate mode.
2001                 let pc = [0.0f32; PUSH_CONSTANT_FLOATS];
2002                 self.core.device.cmd_push_constants(
2003                     cmd,
2004                     self.pipeline_layout,
2005                     vk::ShaderStageFlags::FRAGMENT,
2006                     0,
2007                     bytemuck::cast_slice(&pc),
2008                 );
2009                 self.core.device
2010                     .cmd_draw(cmd, frame.overlay_count, 1, frame.overlay_start, 0);
2011             }
2012             self.core.device.cmd_end_render_pass(cmd);
2013             self.core.device.end_command_buffer(cmd).unwrap();
2014 
2015             // Submit + present. The acquire semaphore gates the swapchain image's
2016             // first use: the backdrop copy (TRANSFER) or the UI pass (COLOR).
2017             let wait_semaphores = [image_available];
2018             let wait_stages = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
2019                 | vk::PipelineStageFlags::TRANSFER];
2020             let cmds = [cmd];
2021             let signal_semaphores = [self.render_finished[image_index as usize]];
2022             let submit = vk::SubmitInfo::default()
2023                 .wait_semaphores(&wait_semaphores)
2024                 .wait_dst_stage_mask(&wait_stages)
2025                 .command_buffers(&cmds)
2026                 .signal_semaphores(&signal_semaphores);
2027             self.core.device
2028                 .queue_submit(self.core.queue, &[submit], in_flight)
2029                 .expect("Queue submit failed");
2030 
2031             let swapchains = [self.swapchain];
2032             let image_indices = [image_index];
2033             let present = vk::PresentInfoKHR::default()
2034                 .wait_semaphores(&signal_semaphores)
2035                 .swapchains(&swapchains)
2036                 .image_indices(&image_indices);
2037             if present_debug() {
2038                 eprintln!("[vk] frame {} present img {}...", self.present_debug_count, image_index);
2039             }
2040             match self.swapchain_loader.queue_present(self.core.queue, &present) {
2041                 Ok(suboptimal) => {
2042                     if suboptimal {
2043                         self.swapchain_dirty = true;
2044                     }
2045                 }
2046                 Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
2047                     self.swapchain_dirty = true;
2048                 }
2049                 Err(e) => log::error!("queue_present failed: {e:?}"),
2050             }
2051 
2052             if present_debug() {
2053                 eprintln!("[vk] frame {} presented", self.present_debug_count);
2054                 self.present_debug_count += 1;
2055             }
2056             self.frame_index = (self.frame_index + 1) % FRAMES_IN_FLIGHT;
2057         }
2058         true
2059     }
2060 }
2061 
2062 /// `CCE_PRESENT_DEBUG=1` traces every acquire/present to stderr — the
2063 /// diagnostic for present-pipeline stalls (a present that logs `acquire...`
2064 /// or `present img N...` with no matching completion line is blocked inside
2065 /// the driver; see the off-viewport freeze notes on the present-mode choice
2066 /// in `create_swapchain`).
2067 fn present_debug() -> bool {
2068     static FLAG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2069     *FLAG.get_or_init(|| std::env::var_os("CCE_PRESENT_DEBUG").is_some())
2070 }
2071 
2072 impl Drop for VkRenderer {
2073     fn drop(&mut self) {
2074         unsafe {
2075             let _ = self.core.device.device_wait_idle();
2076 
2077             let mut frames = std::mem::take(&mut self.frames);
2078             for frame in &mut frames {
2079                 self.core.device.destroy_semaphore(frame.image_available, None);
2080                 self.core.device.destroy_fence(frame.in_flight, None);
2081                 let mut vertex = std::mem::replace(&mut frame.vertex, AllocatedBuffer::null());
2082                 if let Some(allocator) = self.core.allocator.as_mut() {
2083                     destroy_cpu_buffer(&self.core.device, allocator, &mut vertex);
2084                 }
2085             }
2086 
2087             self.destroy_swapchain_resources();
2088             if self.swapchain != vk::SwapchainKHR::null() {
2089                 self.swapchain_loader.destroy_swapchain(self.swapchain, None);
2090             }
2091 
2092             if let Some(allocator) = self.core.allocator.as_mut() {
2093                 self.text.destroy(&self.core.device, allocator);
2094             }
2095 
2096             self.core.device.destroy_sampler(self.backdrop_sampler, None);
2097             if self.snapshot_view != vk::ImageView::null() {
2098                 self.core.device.destroy_image_view(self.snapshot_view, None);
2099                 self.core.device.destroy_image(self.snapshot_image, None);
2100             }
2101             if let Some(alloc) = self.snapshot_allocation.take() {
2102                 if let Some(allocator) = self.core.allocator.as_mut() {
2103                     let _ = allocator.free(alloc);
2104                 }
2105             }
2106             if let Some(allocator) = self.core.allocator.as_mut() {
2107                 self.scene.destroy(&self.core.device, allocator);
2108                 self.image.destroy(&self.core.device, allocator);
2109                 if let Some(mut rt) = self.rt.take() {
2110                     rt.destroy(&self.core.device, allocator);
2111                 }
2112             }
2113             let mut window_info = std::mem::replace(&mut self.window_info, AllocatedBuffer::null());
2114             let mut plate_features =
2115                 std::mem::replace(&mut self.plate_features, AllocatedBuffer::null());
2116             if let Some(allocator) = self.core.allocator.as_mut() {
2117                 destroy_cpu_buffer(&self.core.device, allocator, &mut window_info);
2118                 destroy_cpu_buffer(&self.core.device, allocator, &mut plate_features);
2119             }
2120 
2121             self.core.device.destroy_descriptor_pool(self.descriptor_pool, None);
2122             self.core.device
2123                 .destroy_descriptor_set_layout(self.descriptor_set_layout, None);
2124             self.core.device.destroy_pipeline(self.pipeline, None);
2125             self.core.device.destroy_pipeline_layout(self.pipeline_layout, None);
2126             self.core.device.destroy_shader_module(self.shader_module, None);
2127             self.core.device.destroy_render_pass(self.render_pass, None);
2128             self.core.device.destroy_render_pass(self.render_pass_load, None);
2129             self.core.surface_loader.destroy_surface(self.surface, None);
2130             // The rest (allocator, command pool, device, instance) is the
2131             // core's Drop, which runs after this body.
2132         }
2133     }
2134 }
2135 
2136 #[cfg(test)]
2137 mod tests {
2138     /// The WGSL shaders compile at process start, so a syntax or validation
2139     /// error is a runtime panic in every client — catch it headlessly here.
2140     #[test]
2141     fn shader2d_compiles() {
2142         assert!(!super::shader2d_spirv().is_empty());
2143     }
2144 
2145     #[test]
2146     fn scene3d_compiles() {
2147         assert!(!super::scene3d_spirv().is_empty());
2148     }
2149 
2150     /// `WINDOW_INFO_BYTES` sizes the uniform buffer AND its descriptor range,
2151     /// and `write_window_info` addresses it by float index — all three have to
2152     /// agree with shader2d's `WindowInfo` struct, and nothing but a comment
2153     /// said so. A field appended to the WGSL without growing the const writes
2154     /// the new value past the end of the buffer, which is a validation error
2155     /// on a good day and a garbage uniform on a bad one.
2156     ///
2157     /// Reads the struct out of the shader source rather than duplicating its
2158     /// shape here, so it measures the thing it is guarding.
2159     #[test]
2160     fn window_info_layout_matches_the_uniform_size() {
2161         let src = include_str!("shader2d.wgsl");
2162         let body = src
2163             .split_once("struct WindowInfo {")
2164             .expect("WindowInfo moved; this test scans for it")
2165             .1
2166             .split_once("\n}")
2167             .expect("unterminated WindowInfo")
2168             .0;
2169 
2170         let mut floats = 0usize;
2171         for line in body.lines() {
2172             let line = line.trim();
2173             if line.is_empty() || line.starts_with("//") {
2174                 continue;
2175             }
2176             let ty = line.split_once(':').expect("field: type").1.trim().trim_end_matches(',');
2177             floats += match ty {
2178                 "f32" => 1,
2179                 "vec2<f32>" | "vec2f" => 2,
2180                 "vec4<f32>" | "vec4f" => 4,
2181                 // std140-ish: an array of vec4 is its element count x 4.
2182                 t if t.starts_with("array<vec4f,") => {
2183                     let n: usize = t
2184                         .trim_start_matches("array<vec4f,")
2185                         .trim_end_matches('>')
2186                         .trim()
2187                         .parse()
2188                         .expect("array length");
2189                     n * 4
2190                 }
2191                 other => panic!("WindowInfo field type {other} is not in this test's size table"),
2192             };
2193         }
2194 
2195         assert_eq!(
2196             floats * 4,
2197             super::WINDOW_INFO_BYTES as usize,
2198             "WindowInfo is {floats} floats ({} bytes); WINDOW_INFO_BYTES says {}",
2199             floats * 4,
2200             super::WINDOW_INFO_BYTES,
2201         );
2202     }
2203 }