GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/vk/scene.rs (39.9K)
1 //! 3D scene stage: the ash port of the app's "3D canvas render pass". Draws
2 //! Vertex3D meshes (shader_3d.wgsl: mvp transform, z=9.99 background-quad
3 //! special case, window-corner discard) into the full-size backdrop image with
4 //! a depth buffer, scissored to the viewport pane. The renderer then copies the
5 //! backdrop into the swapchain image and draws the UI pass over it — the same
6 //! image doubles as the blur-behind source for the 2D shader, replacing
7 //! milestone 1's 1x1 placeholder.
8 //!
9 //! Meshes are handle-based (`MeshId`); per-draw uniforms (mvp + window info) go
10 //! into one dynamic-offset uniform buffer per frame in flight, so a frame's
11 //! draws share a single descriptor set.
12
13 use ash::vk;
14 use gpu_allocator::vulkan::{Allocation, AllocationCreateDesc, AllocationScheme, Allocator};
15 use gpu_allocator::MemoryLocation;
16
17 use super::renderer::{create_cpu_buffer, destroy_cpu_buffer, AllocatedBuffer};
18
19 /// Layout-identical to the app's `geometry::Vertex3D` (bytemuck-castable at cutover).
20 #[repr(C)]
21 #[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
22 pub struct Vertex3D {
23 pub position: [f32; 3],
24 pub color: [f32; 3],
25 }
26
27 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
28 pub struct MeshId(usize);
29
30 /// One draw in the staged scene: a mesh under an mvp. The window-size/radius
31 /// tail of shader_3d's uniform block is filled in by the renderer.
32 pub struct SceneDraw {
33 pub mesh: MeshId,
34 pub mvp: [[f32; 4]; 4],
35 /// Rasterize through the line pipeline (LINE_LIST topology): the mesh
36 /// must be an EDGE mesh (vertex pairs), not the triangle fill mesh.
37 pub wireframe: bool,
38 /// rgb + mix: the fragment color is mixed toward `wire_tint.rgb` by
39 /// `wire_tint[3]`. Zero = vertex colors untouched (the default draw).
40 /// A wireframe pass overlaid on its own filled mesh needs this — the
41 /// lines inherit the mesh's colors and would otherwise vanish into the
42 /// identical fill beneath.
43 pub wire_tint: [f32; 4],
44 /// Whole-draw alpha multiplier (1.0 = opaque). The pass blends with
45 /// straight alpha, so translucent draws show whatever rendered beneath.
46 pub opacity: f32,
47 /// Rasterized line width in framebuffer pixels for wireframe draws
48 /// (ignored on fills). Clamped to the device's wideLines cap — 1.0
49 /// everywhere when the feature is absent.
50 pub line_width: f32,
51 /// FILL draws only: the width of a wire pass that will ride on this
52 /// fill (0 = none). The fill is pushed back by its own slope-scaled
53 /// polygon offset sized to that width, so the coplanar wires win the
54 /// depth test solidly: a w-px line samples the fill's plane up to
55 /// (w/2 + 0.5) px off the true edge, and biasing the LINE can't cover
56 /// that (its own depth slope is along-axis — near zero for
57 /// contour-following wires) while the fill's slope is exactly the
58 /// quantity needed.
59 pub wire_base_width: f32,
60 /// FILL draws only: the vertex colours already carry their lighting, so
61 /// the fragment shader skips its derivative-normal flat shading and
62 /// draws them as they are. A host that wants SMOOTH shading bakes it —
63 /// the light is fixed in world space (see `scene3d.wgsl`), so lighting
64 /// per vertex from interpolated normals is exact for a static light,
65 /// and the vertex format needs no normal. False for an ordinary draw.
66 pub prelit: bool,
67 }
68
69 /// shader_3d.wgsl's uniform block.
70 #[repr(C)]
71 #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
72 struct SceneUniforms {
73 mvp: [[f32; 4]; 4],
74 window_size: [f32; 2],
75 window_radius: f32,
76 corner_shape: f32,
77 wire_tint: [f32; 4],
78 opacity: f32,
79 /// 1.0 on wireframe draws: the fragment shader skips the derivative-
80 /// normal flat shading, whose screen-space derivatives are degenerate on
81 /// line fragments (along-axis only) and light the wires with noise.
82 is_wire: f32,
83 /// 1.0 on `SceneDraw::prelit` draws: the flat shading is skipped too.
84 prelit: f32,
85 _pad: [f32; 1],
86 }
87
88 const UNIFORM_SIZE: vk::DeviceSize = std::mem::size_of::<SceneUniforms>() as vk::DeviceSize;
89
90 struct Mesh {
91 buffer: AllocatedBuffer,
92 count: u32,
93 }
94
95 struct StagedScene {
96 scissor: (u32, u32, u32, u32),
97 draws: Vec<SceneDraw>,
98 }
99
100 struct SceneFrame {
101 uniforms: AllocatedBuffer,
102 descriptor_set: vk::DescriptorSet,
103 draw_count: u32,
104 }
105
106 pub(crate) struct SceneStage {
107 render_pass: vk::RenderPass,
108 pipeline: vk::Pipeline,
109 /// PolygonMode::LINE twin of `pipeline` — None when the device lacks
110 /// fillModeNonSolid (wireframe draws then fall back to the fill pipeline).
111 wireframe_pipeline: Option<vk::Pipeline>,
112 /// Device cap for `SceneDraw::line_width` (1.0 without wideLines).
113 max_line_width: f32,
114 pipeline_layout: vk::PipelineLayout,
115 descriptor_set_layout: vk::DescriptorSetLayout,
116 descriptor_pool: vk::DescriptorPool,
117 shader_module: vk::ShaderModule,
118 uniform_stride: vk::DeviceSize,
119
120 format: vk::Format,
121 extent: vk::Extent2D,
122 pub(crate) backdrop_image: vk::Image,
123 pub(crate) backdrop_view: vk::ImageView,
124 backdrop_allocation: Option<Allocation>,
125 depth_image: vk::Image,
126 depth_view: vk::ImageView,
127 depth_allocation: Option<Allocation>,
128 framebuffer: vk::Framebuffer,
129
130 meshes: Vec<Mesh>,
131 frames: Vec<SceneFrame>,
132 staged: Option<StagedScene>,
133 /// True once the backdrop holds rendered content worth copying to screen.
134 pub(crate) backdrop_valid: bool,
135 }
136
137 impl SceneStage {
138 pub(crate) fn new(
139 device: &ash::Device,
140 allocator: &mut Allocator,
141 format: vk::Format,
142 extent: vk::Extent2D,
143 frames_in_flight: usize,
144 min_uniform_align: vk::DeviceSize,
145 max_line_width: f32,
146 ) -> Self {
147 unsafe {
148 // Offscreen pass: color -> TRANSFER_SRC (copied to the swapchain
149 // right after), depth is transient.
150 let attachments = [
151 vk::AttachmentDescription::default()
152 .format(format)
153 .samples(vk::SampleCountFlags::TYPE_1)
154 .load_op(vk::AttachmentLoadOp::CLEAR)
155 .store_op(vk::AttachmentStoreOp::STORE)
156 .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
157 .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
158 .initial_layout(vk::ImageLayout::UNDEFINED)
159 .final_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL),
160 vk::AttachmentDescription::default()
161 .format(vk::Format::D32_SFLOAT)
162 .samples(vk::SampleCountFlags::TYPE_1)
163 .load_op(vk::AttachmentLoadOp::CLEAR)
164 .store_op(vk::AttachmentStoreOp::DONT_CARE)
165 .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
166 .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
167 .initial_layout(vk::ImageLayout::UNDEFINED)
168 .final_layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL),
169 ];
170 let color_refs = [vk::AttachmentReference::default()
171 .attachment(0)
172 .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)];
173 let depth_ref = vk::AttachmentReference::default()
174 .attachment(1)
175 .layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
176 let subpasses = [vk::SubpassDescription::default()
177 .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
178 .color_attachments(&color_refs)
179 .depth_stencil_attachment(&depth_ref)];
180 let dependencies = [
181 // Prior frame sampled the backdrop (blur plates) and used the depth
182 // image; execution dependency before we overwrite from UNDEFINED.
183 vk::SubpassDependency::default()
184 .src_subpass(vk::SUBPASS_EXTERNAL)
185 .dst_subpass(0)
186 .src_stage_mask(
187 vk::PipelineStageFlags::FRAGMENT_SHADER
188 | vk::PipelineStageFlags::LATE_FRAGMENT_TESTS,
189 )
190 .src_access_mask(vk::AccessFlags::empty())
191 .dst_stage_mask(
192 vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
193 | vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS,
194 )
195 .dst_access_mask(
196 vk::AccessFlags::COLOR_ATTACHMENT_WRITE
197 | vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE,
198 ),
199 // The copy to the swapchain reads the color attachment right after.
200 vk::SubpassDependency::default()
201 .src_subpass(0)
202 .dst_subpass(vk::SUBPASS_EXTERNAL)
203 .src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
204 .src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
205 .dst_stage_mask(vk::PipelineStageFlags::TRANSFER)
206 .dst_access_mask(vk::AccessFlags::TRANSFER_READ),
207 ];
208 let render_pass = device
209 .create_render_pass(
210 &vk::RenderPassCreateInfo::default()
211 .attachments(&attachments)
212 .subpasses(&subpasses)
213 .dependencies(&dependencies),
214 None,
215 )
216 .expect("Failed to create scene render pass");
217
218 let bindings = [vk::DescriptorSetLayoutBinding::default()
219 .binding(0)
220 .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
221 .descriptor_count(1)
222 .stage_flags(vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT)];
223 let descriptor_set_layout = device
224 .create_descriptor_set_layout(
225 &vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
226 None,
227 )
228 .expect("Failed to create scene descriptor set layout");
229 let set_layouts_one = [descriptor_set_layout];
230 let pipeline_layout = device
231 .create_pipeline_layout(
232 &vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts_one),
233 None,
234 )
235 .expect("Failed to create scene pipeline layout");
236
237 let spirv = super::renderer::scene3d_spirv();
238 let shader_module = device
239 .create_shader_module(&vk::ShaderModuleCreateInfo::default().code(spirv), None)
240 .expect("Failed to create 3D shader module");
241 let stages = [
242 vk::PipelineShaderStageCreateInfo::default()
243 .stage(vk::ShaderStageFlags::VERTEX)
244 .module(shader_module)
245 .name(c"vs_main"),
246 vk::PipelineShaderStageCreateInfo::default()
247 .stage(vk::ShaderStageFlags::FRAGMENT)
248 .module(shader_module)
249 .name(c"fs_main"),
250 ];
251 let vertex_bindings = [vk::VertexInputBindingDescription::default()
252 .binding(0)
253 .stride(std::mem::size_of::<Vertex3D>() as u32)
254 .input_rate(vk::VertexInputRate::VERTEX)];
255 let vertex_attributes = [
256 vk::VertexInputAttributeDescription::default()
257 .location(0)
258 .binding(0)
259 .format(vk::Format::R32G32B32_SFLOAT)
260 .offset(0),
261 vk::VertexInputAttributeDescription::default()
262 .location(1)
263 .binding(0)
264 .format(vk::Format::R32G32B32_SFLOAT)
265 .offset(12),
266 ];
267 let vertex_input = vk::PipelineVertexInputStateCreateInfo::default()
268 .vertex_binding_descriptions(&vertex_bindings)
269 .vertex_attribute_descriptions(&vertex_attributes);
270 let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::default()
271 .topology(vk::PrimitiveTopology::TRIANGLE_LIST);
272 let viewport_state = vk::PipelineViewportStateCreateInfo::default()
273 .viewport_count(1)
274 .scissor_count(1);
275 // wgpu pipeline_3d: CCW front, back-face culling. Winding survives
276 // because the renderer flips Y via negative viewport height (like
277 // wgpu-hal), not in the shader. Depth bias is enabled but DYNAMIC
278 // (zero for ordinary fills): fills carrying a wire overlay are
279 // pushed back per `SceneDraw::wire_base_width`.
280 let rasterization = vk::PipelineRasterizationStateCreateInfo::default()
281 .polygon_mode(vk::PolygonMode::FILL)
282 .cull_mode(vk::CullModeFlags::BACK)
283 .front_face(vk::FrontFace::COUNTER_CLOCKWISE)
284 .depth_bias_enable(true)
285 .line_width(1.0);
286 let multisample = vk::PipelineMultisampleStateCreateInfo::default()
287 .rasterization_samples(vk::SampleCountFlags::TYPE_1);
288 let depth_stencil = vk::PipelineDepthStencilStateCreateInfo::default()
289 .depth_test_enable(true)
290 .depth_write_enable(true)
291 .depth_compare_op(vk::CompareOp::LESS);
292 let blend_attachments = [vk::PipelineColorBlendAttachmentState::default()
293 .blend_enable(true)
294 .src_color_blend_factor(vk::BlendFactor::SRC_ALPHA)
295 .dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
296 .color_blend_op(vk::BlendOp::ADD)
297 .src_alpha_blend_factor(vk::BlendFactor::ONE)
298 .dst_alpha_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
299 .alpha_blend_op(vk::BlendOp::ADD)
300 .color_write_mask(vk::ColorComponentFlags::RGBA)];
301 let color_blend = vk::PipelineColorBlendStateCreateInfo::default()
302 .attachments(&blend_attachments);
303 let dynamic_states = [
304 vk::DynamicState::VIEWPORT,
305 vk::DynamicState::SCISSOR,
306 vk::DynamicState::DEPTH_BIAS,
307 ];
308 let dynamic_state =
309 vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
310 let pipeline = device
311 .create_graphics_pipelines(
312 vk::PipelineCache::null(),
313 &[vk::GraphicsPipelineCreateInfo::default()
314 .stages(&stages)
315 .vertex_input_state(&vertex_input)
316 .input_assembly_state(&input_assembly)
317 .viewport_state(&viewport_state)
318 .rasterization_state(&rasterization)
319 .multisample_state(&multisample)
320 .depth_stencil_state(&depth_stencil)
321 .color_blend_state(&color_blend)
322 .dynamic_state(&dynamic_state)
323 .layout(pipeline_layout)
324 .render_pass(render_pass)
325 .subpass(0)],
326 None,
327 )
328 .expect("Failed to create 3D pipeline")[0];
329
330 // The wireframe twin draws LINE_LIST edge meshes, NOT the fill
331 // mesh through PolygonMode::LINE. Polygon-mode lines proved
332 // driver-broken twice on Mesa ANV with the negative-height
333 // viewport: triangle winding is evaluated without the
334 // framebuffer Y-mirror (CCW-front selected the FAR facet set —
335 // wireframe spheres drew only the far hemisphere's interior,
336 // pole-fan forensics), and vertex-attribute sourcing fetches
337 // from the wrong vertices (wires aligned to the mesh but carried
338 // colors from a rotated region — the sphere's symmetry masked
339 // the misplacement geometrically). Real line primitives take the
340 // ordinary, well-tested raster path: no facet culling exists, so
341 // hidden-wire removal is the DEPTH test against the fill, which
342 // `SceneDraw::wire_base_width` pushes back.
343 // The compare is LESS_OR_EQUAL with writes off, and the wires
344 // carry NO bias — the tiebreak lives on the FILL side
345 // (`SceneDraw::wire_base_width` pushes the fill back by its own
346 // slope-scaled polygon offset). Biasing the line cannot work for
347 // wide wires: a w-px line's fragments sample the fill's plane up
348 // to (w/2 + 0.5) px off the true edge, but the hardware scales a
349 // line's slope bias by its ALONG-AXIS depth slope — near zero
350 // for contour-following wires — while strong constant terms
351 // punch FAR-side wires through the near fill (the old -4/-1 did
352 // exactly that; with the culled-facet flip above those far wires
353 // were the only wires, and the overlay's whole lattice was the
354 // back side showing through, which read as the mesh
355 // counter-rotating during orbits).
356 let depth_stencil_lines = vk::PipelineDepthStencilStateCreateInfo::default()
357 .depth_test_enable(true)
358 .depth_write_enable(false)
359 .depth_compare_op(vk::CompareOp::LESS_OR_EQUAL);
360 let wireframe_pipeline = Some({
361 let input_assembly_lines = vk::PipelineInputAssemblyStateCreateInfo::default()
362 .topology(vk::PrimitiveTopology::LINE_LIST);
363 let rasterization_lines = vk::PipelineRasterizationStateCreateInfo::default()
364 .polygon_mode(vk::PolygonMode::FILL)
365 .cull_mode(vk::CullModeFlags::NONE)
366 .front_face(vk::FrontFace::COUNTER_CLOCKWISE)
367 .depth_bias_enable(true)
368 .line_width(1.0);
369 // Line width is dynamic (SceneDraw::line_width); depth bias
370 // is dynamic on both pipelines and set to zero for wires —
371 // see the comment above.
372 let dynamic_states_lines = [
373 vk::DynamicState::VIEWPORT,
374 vk::DynamicState::SCISSOR,
375 vk::DynamicState::LINE_WIDTH,
376 vk::DynamicState::DEPTH_BIAS,
377 ];
378 let dynamic_state_lines = vk::PipelineDynamicStateCreateInfo::default()
379 .dynamic_states(&dynamic_states_lines);
380 device
381 .create_graphics_pipelines(
382 vk::PipelineCache::null(),
383 &[vk::GraphicsPipelineCreateInfo::default()
384 .stages(&stages)
385 .vertex_input_state(&vertex_input)
386 .input_assembly_state(&input_assembly_lines)
387 .viewport_state(&viewport_state)
388 .rasterization_state(&rasterization_lines)
389 .multisample_state(&multisample)
390 .depth_stencil_state(&depth_stencil_lines)
391 .color_blend_state(&color_blend)
392 .dynamic_state(&dynamic_state_lines)
393 .layout(pipeline_layout)
394 .render_pass(render_pass)
395 .subpass(0)],
396 None,
397 )
398 .expect("Failed to create 3D wireframe pipeline")[0]
399 });
400
401 let uniform_stride = UNIFORM_SIZE.next_multiple_of(min_uniform_align.max(1));
402
403 let pool_sizes = [vk::DescriptorPoolSize::default()
404 .ty(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
405 .descriptor_count(frames_in_flight as u32)];
406 let descriptor_pool = device
407 .create_descriptor_pool(
408 &vk::DescriptorPoolCreateInfo::default()
409 .max_sets(frames_in_flight as u32)
410 .pool_sizes(&pool_sizes),
411 None,
412 )
413 .expect("Failed to create scene descriptor pool");
414 let set_layouts: Vec<vk::DescriptorSetLayout> =
415 vec![descriptor_set_layout; frames_in_flight];
416 let sets = device
417 .allocate_descriptor_sets(
418 &vk::DescriptorSetAllocateInfo::default()
419 .descriptor_pool(descriptor_pool)
420 .set_layouts(&set_layouts),
421 )
422 .expect("Failed to allocate scene descriptor sets");
423 let frames: Vec<SceneFrame> = sets
424 .into_iter()
425 .map(|descriptor_set| {
426 let uniforms = create_cpu_buffer(
427 device,
428 allocator,
429 uniform_stride * 16,
430 vk::BufferUsageFlags::UNIFORM_BUFFER,
431 "scene-uniforms",
432 );
433 SceneFrame { uniforms, descriptor_set, draw_count: 0 }
434 })
435 .collect();
436 for frame in &frames {
437 Self::write_descriptor(device, frame);
438 }
439
440 let mut stage = SceneStage {
441 render_pass,
442 pipeline,
443 wireframe_pipeline,
444 max_line_width,
445 pipeline_layout,
446 descriptor_set_layout,
447 descriptor_pool,
448 shader_module,
449 uniform_stride,
450 format,
451 extent: vk::Extent2D { width: 0, height: 0 },
452 backdrop_image: vk::Image::null(),
453 backdrop_view: vk::ImageView::null(),
454 backdrop_allocation: None,
455 depth_image: vk::Image::null(),
456 depth_view: vk::ImageView::null(),
457 depth_allocation: None,
458 framebuffer: vk::Framebuffer::null(),
459 meshes: Vec::new(),
460 frames,
461 staged: None,
462 backdrop_valid: false,
463 };
464 stage.resize(device, allocator, extent);
465 stage
466 }
467 }
468
469 fn write_descriptor(device: &ash::Device, frame: &SceneFrame) {
470 let buffer_infos = [vk::DescriptorBufferInfo::default()
471 .buffer(frame.uniforms.buffer)
472 .offset(0)
473 .range(UNIFORM_SIZE)];
474 unsafe {
475 device.update_descriptor_sets(
476 &[vk::WriteDescriptorSet::default()
477 .dst_set(frame.descriptor_set)
478 .dst_binding(0)
479 .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
480 .buffer_info(&buffer_infos)],
481 &[],
482 );
483 }
484 }
485
486 fn destroy_targets(&mut self, device: &ash::Device, allocator: &mut Allocator) {
487 unsafe {
488 if self.framebuffer != vk::Framebuffer::null() {
489 device.destroy_framebuffer(self.framebuffer, None);
490 self.framebuffer = vk::Framebuffer::null();
491 }
492 if self.backdrop_view != vk::ImageView::null() {
493 device.destroy_image_view(self.backdrop_view, None);
494 device.destroy_image(self.backdrop_image, None);
495 self.backdrop_view = vk::ImageView::null();
496 self.backdrop_image = vk::Image::null();
497 }
498 if self.depth_view != vk::ImageView::null() {
499 device.destroy_image_view(self.depth_view, None);
500 device.destroy_image(self.depth_image, None);
501 self.depth_view = vk::ImageView::null();
502 self.depth_image = vk::Image::null();
503 }
504 }
505 if let Some(a) = self.backdrop_allocation.take() {
506 let _ = allocator.free(a);
507 }
508 if let Some(a) = self.depth_allocation.take() {
509 let _ = allocator.free(a);
510 }
511 }
512
513 /// (Re)create the backdrop + depth targets at `extent`. Caller must have the
514 /// device idle (the renderer's swapchain-rebuild path guarantees it) and must
515 /// re-point the UI descriptor at the new `backdrop_view` and re-init its layout.
516 pub(crate) fn resize(
517 &mut self,
518 device: &ash::Device,
519 allocator: &mut Allocator,
520 extent: vk::Extent2D,
521 ) {
522 if extent == self.extent && self.framebuffer != vk::Framebuffer::null() {
523 return;
524 }
525 self.destroy_targets(device, allocator);
526 self.extent = extent;
527 self.backdrop_valid = false;
528 unsafe {
529 let backdrop_image = device
530 .create_image(
531 &vk::ImageCreateInfo::default()
532 .image_type(vk::ImageType::TYPE_2D)
533 .format(self.format)
534 .extent(vk::Extent3D {
535 width: extent.width,
536 height: extent.height,
537 depth: 1,
538 })
539 .mip_levels(1)
540 .array_layers(1)
541 .samples(vk::SampleCountFlags::TYPE_1)
542 .tiling(vk::ImageTiling::OPTIMAL)
543 .usage(
544 vk::ImageUsageFlags::COLOR_ATTACHMENT
545 | vk::ImageUsageFlags::SAMPLED
546 | vk::ImageUsageFlags::TRANSFER_SRC
547 | vk::ImageUsageFlags::TRANSFER_DST,
548 )
549 .initial_layout(vk::ImageLayout::UNDEFINED),
550 None,
551 )
552 .expect("Failed to create backdrop image");
553 let requirements = device.get_image_memory_requirements(backdrop_image);
554 let allocation = allocator
555 .allocate(&AllocationCreateDesc {
556 name: "backdrop",
557 requirements,
558 location: MemoryLocation::GpuOnly,
559 linear: false,
560 allocation_scheme: AllocationScheme::GpuAllocatorManaged,
561 })
562 .expect("Failed to allocate backdrop memory");
563 device
564 .bind_image_memory(backdrop_image, allocation.memory(), allocation.offset())
565 .expect("Failed to bind backdrop memory");
566 let backdrop_view = device
567 .create_image_view(
568 &vk::ImageViewCreateInfo::default()
569 .image(backdrop_image)
570 .view_type(vk::ImageViewType::TYPE_2D)
571 .format(self.format)
572 .subresource_range(
573 vk::ImageSubresourceRange::default()
574 .aspect_mask(vk::ImageAspectFlags::COLOR)
575 .level_count(1)
576 .layer_count(1),
577 ),
578 None,
579 )
580 .expect("Failed to create backdrop view");
581 self.backdrop_image = backdrop_image;
582 self.backdrop_view = backdrop_view;
583 self.backdrop_allocation = Some(allocation);
584
585 let depth_image = device
586 .create_image(
587 &vk::ImageCreateInfo::default()
588 .image_type(vk::ImageType::TYPE_2D)
589 .format(vk::Format::D32_SFLOAT)
590 .extent(vk::Extent3D {
591 width: extent.width,
592 height: extent.height,
593 depth: 1,
594 })
595 .mip_levels(1)
596 .array_layers(1)
597 .samples(vk::SampleCountFlags::TYPE_1)
598 .tiling(vk::ImageTiling::OPTIMAL)
599 .usage(vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT)
600 .initial_layout(vk::ImageLayout::UNDEFINED),
601 None,
602 )
603 .expect("Failed to create depth image");
604 let requirements = device.get_image_memory_requirements(depth_image);
605 let allocation = allocator
606 .allocate(&AllocationCreateDesc {
607 name: "depth",
608 requirements,
609 location: MemoryLocation::GpuOnly,
610 linear: false,
611 allocation_scheme: AllocationScheme::GpuAllocatorManaged,
612 })
613 .expect("Failed to allocate depth memory");
614 device
615 .bind_image_memory(depth_image, allocation.memory(), allocation.offset())
616 .expect("Failed to bind depth memory");
617 let depth_view = device
618 .create_image_view(
619 &vk::ImageViewCreateInfo::default()
620 .image(depth_image)
621 .view_type(vk::ImageViewType::TYPE_2D)
622 .format(vk::Format::D32_SFLOAT)
623 .subresource_range(
624 vk::ImageSubresourceRange::default()
625 .aspect_mask(vk::ImageAspectFlags::DEPTH)
626 .level_count(1)
627 .layer_count(1),
628 ),
629 None,
630 )
631 .expect("Failed to create depth view");
632 self.depth_image = depth_image;
633 self.depth_view = depth_view;
634 self.depth_allocation = Some(allocation);
635
636 let attachments = [self.backdrop_view, self.depth_view];
637 self.framebuffer = device
638 .create_framebuffer(
639 &vk::FramebufferCreateInfo::default()
640 .render_pass(self.render_pass)
641 .attachments(&attachments)
642 .width(extent.width)
643 .height(extent.height)
644 .layers(1),
645 None,
646 )
647 .expect("Failed to create scene framebuffer");
648 }
649 }
650
651 pub(crate) fn create_mesh(
652 &mut self,
653 device: &ash::Device,
654 allocator: &mut Allocator,
655 verts: &[Vertex3D],
656 ) -> MeshId {
657 let bytes: &[u8] = bytemuck::cast_slice(verts);
658 let mut buffer = create_cpu_buffer(
659 device,
660 allocator,
661 (bytes.len() as vk::DeviceSize).max(64),
662 vk::BufferUsageFlags::VERTEX_BUFFER,
663 "mesh",
664 );
665 if !bytes.is_empty() {
666 buffer.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()[..bytes.len()]
667 .copy_from_slice(bytes);
668 }
669 self.meshes.push(Mesh { buffer, count: verts.len() as u32 });
670 MeshId(self.meshes.len() - 1)
671 }
672
673 /// Replace a mesh's vertices. Caller must have the device idle: meshes may be
674 /// referenced by in-flight frames (geometry updates are rare — settings
675 /// changes and graph rebuilds — so a wait is acceptable here).
676 #[allow(dead_code)] // cutover API: the app's rebuild_scene_geometry path
677 pub(crate) fn update_mesh(
678 &mut self,
679 device: &ash::Device,
680 allocator: &mut Allocator,
681 id: MeshId,
682 verts: &[Vertex3D],
683 ) {
684 let mesh = &mut self.meshes[id.0];
685 let bytes: &[u8] = bytemuck::cast_slice(verts);
686 let needed = bytes.len() as vk::DeviceSize;
687 if needed > mesh.buffer.size {
688 let mut old = std::mem::replace(&mut mesh.buffer, AllocatedBuffer::null());
689 destroy_cpu_buffer(device, allocator, &mut old);
690 mesh.buffer = create_cpu_buffer(
691 device,
692 allocator,
693 needed.next_power_of_two(),
694 vk::BufferUsageFlags::VERTEX_BUFFER,
695 "mesh",
696 );
697 }
698 if !bytes.is_empty() {
699 mesh.buffer.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()[..bytes.len()]
700 .copy_from_slice(bytes);
701 }
702 mesh.count = verts.len() as u32;
703 }
704
705 pub(crate) fn stage(&mut self, scissor: (u32, u32, u32, u32), draws: Vec<SceneDraw>) {
706 self.staged = Some(StagedScene { scissor, draws });
707 }
708
709 /// After the frame fence: write this frame's per-draw uniforms (mvp + the
710 /// window-corner info shader_3d shares with the 2D shader).
711 pub(crate) fn write_frame_uniforms(
712 &mut self,
713 device: &ash::Device,
714 allocator: &mut Allocator,
715 frame_index: usize,
716 corner_radius_px: f32,
717 ) {
718 let Some(staged) = &self.staged else {
719 self.frames[frame_index].draw_count = 0;
720 return;
721 };
722 let frame = &mut self.frames[frame_index];
723 let needed = self.uniform_stride * staged.draws.len().max(1) as vk::DeviceSize;
724 if needed > frame.uniforms.size {
725 let mut old = std::mem::replace(&mut frame.uniforms, AllocatedBuffer::null());
726 destroy_cpu_buffer(device, allocator, &mut old);
727 frame.uniforms = create_cpu_buffer(
728 device,
729 allocator,
730 needed.next_power_of_two(),
731 vk::BufferUsageFlags::UNIFORM_BUFFER,
732 "scene-uniforms",
733 );
734 Self::write_descriptor(device, frame);
735 }
736 let window_size = [self.extent.width as f32, self.extent.height as f32];
737 let corner_shape = crate::layout::corner_shape();
738 let mapped = frame.uniforms.allocation.as_mut().unwrap().mapped_slice_mut().unwrap();
739 for (i, draw) in staged.draws.iter().enumerate() {
740 let uniforms = SceneUniforms {
741 mvp: draw.mvp,
742 window_size,
743 window_radius: corner_radius_px,
744 corner_shape,
745 wire_tint: draw.wire_tint,
746 opacity: draw.opacity,
747 is_wire: if draw.wireframe { 1.0 } else { 0.0 },
748 prelit: if draw.prelit { 1.0 } else { 0.0 },
749 _pad: [0.0; 1],
750 };
751 let offset = (self.uniform_stride as usize) * i;
752 mapped[offset..offset + UNIFORM_SIZE as usize]
753 .copy_from_slice(bytemuck::bytes_of(&uniforms));
754 }
755 frame.draw_count = staged.draws.len() as u32;
756 }
757
758 /// Record the offscreen scene pass. Consumes the staged scene; afterwards the
759 /// backdrop is in TRANSFER_SRC layout, ready for the swapchain copy. Returns
760 /// false if nothing was staged.
761 pub(crate) fn record(
762 &mut self,
763 device: &ash::Device,
764 cmd: vk::CommandBuffer,
765 frame_index: usize,
766 ) -> bool {
767 let Some(staged) = self.staged.take() else {
768 return false;
769 };
770 let frame = &self.frames[frame_index];
771 unsafe {
772 let clear_values = [
773 vk::ClearValue { color: vk::ClearColorValue { float32: [0.0, 0.0, 0.0, 0.0] } },
774 vk::ClearValue {
775 depth_stencil: vk::ClearDepthStencilValue { depth: 1.0, stencil: 0 },
776 },
777 ];
778 device.cmd_begin_render_pass(
779 cmd,
780 &vk::RenderPassBeginInfo::default()
781 .render_pass(self.render_pass)
782 .framebuffer(self.framebuffer)
783 .render_area(vk::Rect2D {
784 offset: vk::Offset2D { x: 0, y: 0 },
785 extent: self.extent,
786 })
787 .clear_values(&clear_values),
788 vk::SubpassContents::INLINE,
789 );
790 // Negative-height viewport: wgpu's Y-up NDC without touching winding.
791 device.cmd_set_viewport(
792 cmd,
793 0,
794 &[vk::Viewport {
795 x: 0.0,
796 y: self.extent.height as f32,
797 width: self.extent.width as f32,
798 height: -(self.extent.height as f32),
799 min_depth: 0.0,
800 max_depth: 1.0,
801 }],
802 );
803 let (sx, sy, sw, sh) = staged.scissor;
804 let sx = sx.min(self.extent.width);
805 let sy = sy.min(self.extent.height);
806 device.cmd_set_scissor(
807 cmd,
808 0,
809 &[vk::Rect2D {
810 offset: vk::Offset2D { x: sx as i32, y: sy as i32 },
811 extent: vk::Extent2D {
812 width: sw.min(self.extent.width - sx),
813 height: sh.min(self.extent.height - sy),
814 },
815 }],
816 );
817 let mut bound = self.pipeline;
818 device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, bound);
819 for (i, draw) in staged.draws.iter().enumerate() {
820 let mesh = &self.meshes[draw.mesh.0];
821 if mesh.count == 0 {
822 continue;
823 }
824 let wanted = if draw.wireframe {
825 self.wireframe_pipeline.unwrap_or(self.pipeline)
826 } else {
827 self.pipeline
828 };
829 if wanted != bound {
830 device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, wanted);
831 bound = wanted;
832 }
833 if draw.wireframe && self.wireframe_pipeline.is_some() {
834 device.cmd_set_line_width(cmd, draw.line_width.clamp(1.0, self.max_line_width));
835 device.cmd_set_depth_bias(cmd, 0.0, 0.0, 0.0);
836 } else if draw.wire_base_width > 0.0 {
837 // Push this fill behind its coming wire overlay. The
838 // slope term must cover not just the wires' across-width
839 // sampling offset (w/2 px) but the NEIGHBOR facet's
840 // plane: a wire lies on edge A|B and its fragments carry
841 // A's plane depth, while the fill under the far half of
842 // the wire is B's plane, which on a convex surface tilts
843 // closer — hence the extra pixel of slope headroom.
844 let w = draw.wire_base_width.clamp(1.0, self.max_line_width);
845 device.cmd_set_depth_bias(cmd, 2.0, 0.0, 1.5 + w);
846 } else {
847 device.cmd_set_depth_bias(cmd, 0.0, 0.0, 0.0);
848 }
849 device.cmd_bind_descriptor_sets(
850 cmd,
851 vk::PipelineBindPoint::GRAPHICS,
852 self.pipeline_layout,
853 0,
854 &[frame.descriptor_set],
855 &[(self.uniform_stride as u32) * i as u32],
856 );
857 device.cmd_bind_vertex_buffers(cmd, 0, &[mesh.buffer.buffer], &[0]);
858 device.cmd_draw(cmd, mesh.count, 1, 0, 0);
859 }
860 device.cmd_end_render_pass(cmd);
861 }
862 self.backdrop_valid = true;
863 true
864 }
865
866 pub(crate) fn destroy(&mut self, device: &ash::Device, allocator: &mut Allocator) {
867 self.destroy_targets(device, allocator);
868 unsafe {
869 for frame in &mut self.frames {
870 let mut uniforms = std::mem::replace(&mut frame.uniforms, AllocatedBuffer::null());
871 destroy_cpu_buffer(device, allocator, &mut uniforms);
872 }
873 for mesh in &mut self.meshes {
874 let mut buffer = std::mem::replace(&mut mesh.buffer, AllocatedBuffer::null());
875 destroy_cpu_buffer(device, allocator, &mut buffer);
876 }
877 device.destroy_descriptor_pool(self.descriptor_pool, None);
878 device.destroy_descriptor_set_layout(self.descriptor_set_layout, None);
879 if let Some(p) = self.wireframe_pipeline.take() {
880 device.destroy_pipeline(p, None);
881 }
882 device.destroy_pipeline(self.pipeline, None);
883 device.destroy_pipeline_layout(self.pipeline_layout, None);
884 device.destroy_shader_module(self.shader_module, None);
885 device.destroy_render_pass(self.render_pass, None);
886 }
887 }
888 }