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

commit4e1a2c61b2988ef1d6920a564c2f605139f9e710
parent382bbed640
authorLucas Galante <[email protected]>
date2026-07-21 15:21
feat: wireframe scene pipeline (SceneDraw::wireframe)

fillModeNonSolid is queried and enabled at device creation when
available (VkCore::wireframe_supported); the scene stage builds a
PolygonMode::LINE twin of its fill pipeline and each staged draw picks
one via the new SceneDraw::wireframe flag — falling back to fill on
devices without the feature. Culling stays on so the wire view matches
the fill's visible surface.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/vk/core.rs     | 17 ++++++++++++++++-
 src/vk/renderer.rs |  1 +
 src/vk/scene.rs    | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 68 insertions(+), 2 deletions(-)

diff --git a/src/vk/core.rs b/src/vk/core.rs
index bea6b2f..c1639fd 100644
--- a/src/vk/core.rs
+++ b/src/vk/core.rs
@@ -61,6 +61,9 @@ pub struct VkCore {
     pub(crate) min_uniform_align: vk::DeviceSize,
     /// minAccelerationStructureScratchOffsetAlignment; 1 when no ray-query stack.
     pub(crate) as_scratch_align: vk::DeviceSize,
+    /// fillModeNonSolid was available and enabled — the scene stage may build
+    /// its wireframe (PolygonMode::LINE) pipeline.
+    pub(crate) wireframe_supported: bool,
 }
 
 /// The process-wide Vulkan entry + instance every [`VkCore`] hangs off.
@@ -367,7 +370,18 @@ impl VkCore {
         let mut as_features = vk::PhysicalDeviceAccelerationStructureFeaturesKHR::default()
             .acceleration_structure(true);
         let mut rq_features = vk::PhysicalDeviceRayQueryFeaturesKHR::default().ray_query(true);
-        let mut device_info = vk::DeviceCreateInfo::default().queue_create_infos(&queue_infos);
+        // Non-solid fill (PolygonMode::LINE) for the scene stage's wireframe
+        // pipeline — enabled when the device offers it; consumers check
+        // `wireframe_supported`.
+        let wireframe_supported = instance
+            .get_physical_device_features(physical_device)
+            .fill_mode_non_solid
+            == vk::TRUE;
+        let enabled_features =
+            vk::PhysicalDeviceFeatures::default().fill_mode_non_solid(wireframe_supported);
+        let mut device_info = vk::DeviceCreateInfo::default()
+            .queue_create_infos(&queue_infos)
+            .enabled_features(&enabled_features);
         if ray_query {
             device_extensions.push(ash::khr::acceleration_structure::NAME.as_ptr());
             device_extensions.push(ash::khr::ray_query::NAME.as_ptr());
@@ -431,6 +445,7 @@ impl VkCore {
                 instance,
                 min_uniform_align,
                 as_scratch_align,
+                wireframe_supported,
             },
             surface,
         )
diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
index f20630a..0d5420a 100644
--- a/src/vk/renderer.rs
+++ b/src/vk/renderer.rs
@@ -613,6 +613,7 @@ impl VkRenderer {
             initial_extent,
             FRAMES_IN_FLIGHT,
             min_uniform_align,
+            core.wireframe_supported,
         );
         clear_image_to_shader_read(&device, queue, command_pool, scene.backdrop_image);
 
diff --git a/src/vk/scene.rs b/src/vk/scene.rs
index 0d7bf4b..c0a60e0 100644
--- a/src/vk/scene.rs
+++ b/src/vk/scene.rs
@@ -32,6 +32,9 @@ pub struct MeshId(usize);
 pub struct SceneDraw {
     pub mesh: MeshId,
     pub mvp: [[f32; 4]; 4],
+    /// Rasterize as lines (PolygonMode::LINE) instead of filled triangles.
+    /// Falls back to filled when the device lacks fillModeNonSolid.
+    pub wireframe: bool,
 }
 
 /// shader_3d.wgsl's uniform block.
@@ -65,6 +68,9 @@ struct SceneFrame {
 pub(crate) struct SceneStage {
     render_pass: vk::RenderPass,
     pipeline: vk::Pipeline,
+    /// PolygonMode::LINE twin of `pipeline` — None when the device lacks
+    /// fillModeNonSolid (wireframe draws then fall back to the fill pipeline).
+    wireframe_pipeline: Option<vk::Pipeline>,
     pipeline_layout: vk::PipelineLayout,
     descriptor_set_layout: vk::DescriptorSetLayout,
     descriptor_pool: vk::DescriptorPool,
@@ -96,6 +102,7 @@ impl SceneStage {
         extent: vk::Extent2D,
         frames_in_flight: usize,
         min_uniform_align: vk::DeviceSize,
+        wireframe_supported: bool,
     ) -> Self {
         unsafe {
             // Offscreen pass: color -> TRANSFER_SRC (copied to the swapchain
@@ -273,6 +280,35 @@ impl SceneStage {
                 )
                 .expect("Failed to create 3D pipeline")[0];
 
+            // The wireframe twin: identical but rasterized as lines. Culling
+            // stays on so the wire view matches the fill's visible surface.
+            let wireframe_pipeline = wireframe_supported.then(|| {
+                let rasterization_lines = vk::PipelineRasterizationStateCreateInfo::default()
+                    .polygon_mode(vk::PolygonMode::LINE)
+                    .cull_mode(vk::CullModeFlags::BACK)
+                    .front_face(vk::FrontFace::COUNTER_CLOCKWISE)
+                    .line_width(1.0);
+                device
+                    .create_graphics_pipelines(
+                        vk::PipelineCache::null(),
+                        &[vk::GraphicsPipelineCreateInfo::default()
+                            .stages(&stages)
+                            .vertex_input_state(&vertex_input)
+                            .input_assembly_state(&input_assembly)
+                            .viewport_state(&viewport_state)
+                            .rasterization_state(&rasterization_lines)
+                            .multisample_state(&multisample)
+                            .depth_stencil_state(&depth_stencil)
+                            .color_blend_state(&color_blend)
+                            .dynamic_state(&dynamic_state)
+                            .layout(pipeline_layout)
+                            .render_pass(render_pass)
+                            .subpass(0)],
+                        None,
+                    )
+                    .expect("Failed to create 3D wireframe pipeline")[0]
+            });
+
             let uniform_stride = UNIFORM_SIZE.next_multiple_of(min_uniform_align.max(1));
 
             let pool_sizes = [vk::DescriptorPoolSize::default()
@@ -315,6 +351,7 @@ impl SceneStage {
             let mut stage = SceneStage {
                 render_pass,
                 pipeline,
+                wireframe_pipeline,
                 pipeline_layout,
                 descriptor_set_layout,
                 descriptor_pool,
@@ -682,12 +719,22 @@ impl SceneStage {
                     },
                 }],
             );
-            device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline);
+            let mut bound = self.pipeline;
+            device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, bound);
             for (i, draw) in staged.draws.iter().enumerate() {
                 let mesh = &self.meshes[draw.mesh.0];
                 if mesh.count == 0 {
                     continue;
                 }
+                let wanted = if draw.wireframe {
+                    self.wireframe_pipeline.unwrap_or(self.pipeline)
+                } else {
+                    self.pipeline
+                };
+                if wanted != bound {
+                    device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, wanted);
+                    bound = wanted;
+                }
                 device.cmd_bind_descriptor_sets(
                     cmd,
                     vk::PipelineBindPoint::GRAPHICS,
@@ -718,6 +765,9 @@ impl SceneStage {
             }
             device.destroy_descriptor_pool(self.descriptor_pool, None);
             device.destroy_descriptor_set_layout(self.descriptor_set_layout, None);
+            if let Some(p) = self.wireframe_pipeline.take() {
+                device.destroy_pipeline(p, None);
+            }
             device.destroy_pipeline(self.pipeline, None);
             device.destroy_pipeline_layout(self.pipeline_layout, None);
             device.destroy_shader_module(self.shader_module, None);