git.lucas.co / cce-ui
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 }