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