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 }