GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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);