diff --git a/crates/renderer/src/device.rs b/crates/renderer/src/device.rs index 6fd461a..48ef9d6 100644 --- a/crates/renderer/src/device.rs +++ b/crates/renderer/src/device.rs @@ -53,9 +53,13 @@ pub fn create_logical_device( let mut dynamic_rendering_features = vk::PhysicalDeviceDynamicRenderingFeatures::default().dynamic_rendering(true); + // `fillModeNonSolid` unlocks the `POINT` and `LINE` polygon modes used by the debug render modes. + let enabled_features = vk::PhysicalDeviceFeatures::default().fill_mode_non_solid(true); + let create_info = vk::DeviceCreateInfo::default() .queue_create_infos(std::slice::from_ref(&queue_info)) .enabled_extension_names(&device_extensions) + .enabled_features(&enabled_features) .push_next(&mut synchronization2_features) .push_next(&mut dynamic_rendering_features); diff --git a/crates/renderer/src/lib.rs b/crates/renderer/src/lib.rs index 70e235b..195ec25 100644 --- a/crates/renderer/src/lib.rs +++ b/crates/renderer/src/lib.rs @@ -28,7 +28,7 @@ use raw_window_handle::{RawDisplayHandle, RawWindowHandle}; use std::ffi::c_char; pub use error::RendererError; -pub use renderer::{MeshKey, Renderer}; +pub use renderer::{MeshKey, RenderMode, Renderer}; use std::collections::HashMap; @@ -124,8 +124,17 @@ impl Renderer { // 12. Graphics Pipeline Configuration let pipeline_layout = pipeline::create_pipeline_layout(&device)?; - let graphics_pipeline = - pipeline::create_graphics_pipeline(&device, pipeline_layout, swapchain_format)?; + + // One pipeline per render mode, built up front so switching modes is a bind-time choice rather than a pipeline compilation stall. All variants share `pipeline_layout`; only their rasterisation state differs. + let mut pipelines = [vk::Pipeline::null(); RenderMode::COUNT]; + for mode in RenderMode::ALL { + pipelines[mode.index()] = pipeline::create_graphics_pipeline( + &device, + pipeline_layout, + swapchain_format, + mode.polygon_mode(), + )?; + } let (depth_image, depth_allocation, depth_image_view) = create_depth_resources(&device, &mut allocator, swapchain_extent)?; @@ -155,7 +164,8 @@ impl Renderer { depth_allocation: Some(depth_allocation), depth_image_view, pipeline_layout, - graphics_pipeline, + pipelines, + render_mode: RenderMode::default(), sync: Some(sync), current_frame: 0, }) diff --git a/crates/renderer/src/pipeline.rs b/crates/renderer/src/pipeline.rs index f8aa16b..d0e175d 100644 --- a/crates/renderer/src/pipeline.rs +++ b/crates/renderer/src/pipeline.rs @@ -67,6 +67,7 @@ pub fn create_graphics_pipeline( device: &Device, layout: vk::PipelineLayout, color_format: vk::Format, + polygon_mode: vk::PolygonMode, ) -> Result { // 1. Load and compile shader modules let (vert_module, frag_module) = load_shader_modules(device)?; @@ -101,7 +102,7 @@ pub fn create_graphics_pipeline( let rasterizer = vk::PipelineRasterizationStateCreateInfo::default() .depth_clamp_enable(false) .rasterizer_discard_enable(false) - .polygon_mode(vk::PolygonMode::FILL) + .polygon_mode(polygon_mode) .line_width(1.0) .cull_mode(vk::CullModeFlags::BACK) .front_face(vk::FrontFace::COUNTER_CLOCKWISE) diff --git a/crates/renderer/src/renderer.rs b/crates/renderer/src/renderer.rs index 4f1c557..6813c3e 100644 --- a/crates/renderer/src/renderer.rs +++ b/crates/renderer/src/renderer.rs @@ -10,6 +10,41 @@ use std::collections::HashMap; /// Opaque, renderer-side identifier for one uploaded chunk mesh. pub type MeshKey = (i32, i32, i32); +/// Selects how chunk meshes are rasterised. +/// +/// Non-[`RenderMode::Filled`] variants are debug modes for inspecting the mesher's output; they are not gameplay state. One pipeline is built per variant at initialisation and held in [`Renderer::pipelines`], indexed by [`RenderMode::index`]. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] +pub enum RenderMode { + /// Filled triangles; the normal presentation path. + #[default] + Filled, + /// One point per polygon vertex, exposing the density of the geometry the mesher emitted. + Points, +} + +impl RenderMode { + /// Number of variants, and therefore the number of pipelines built at initialisation. + pub const COUNT: usize = 2; + + /// Every variant, in discriminant order. The array length is checked against [`RenderMode::COUNT`] at compile time, so a new variant that is not listed here fails to build. + pub const ALL: [Self; Self::COUNT] = [Self::Filled, Self::Points]; + + /// Returns the rasterisation polygon mode backing this render mode. + #[must_use] + pub const fn polygon_mode(self) -> vk::PolygonMode { + match self { + Self::Filled => vk::PolygonMode::FILL, + Self::Points => vk::PolygonMode::POINT, + } + } + + /// Returns this mode's position in [`RenderMode::ALL`], used to index [`Renderer::pipelines`]. + #[must_use] + pub const fn index(self) -> usize { + self as usize + } +} + /// GPU resources for a single chunk mesh, drawn at a fixed world offset. pub(crate) struct GpuMesh { /// Buffer holding the chunk's vertex data. @@ -70,8 +105,10 @@ pub struct Renderer { pub(crate) command_buffers: Vec, /// The layout of the graphics pipeline. pub(crate) pipeline_layout: vk::PipelineLayout, - /// The compiled graphics pipeline state. - pub(crate) graphics_pipeline: vk::Pipeline, + /// One compiled pipeline per [`RenderMode`], indexed by [`RenderMode::index`]. All variants share [`Renderer::pipeline_layout`] and differ only in rasterisation state. + pub(crate) pipelines: [vk::Pipeline; RenderMode::COUNT], + /// The rasterisation mode selected for subsequent frames, chosen by [`Renderer::set_render_mode`]. + pub(crate) render_mode: RenderMode, /// Memory manager for GPU allocations. pub(crate) allocator: Option, /// Uploaded chunk meshes, keyed by an opaque renderer-side handle and drawn independently. @@ -385,10 +422,11 @@ impl Renderer { /// Issues the actual draw calls for the frame. fn issue_draw_calls(&self, cmd: vk::CommandBuffer, camera_view: glam::Mat4) { unsafe { + // The bound pipeline is the sole difference between render modes; every other command recorded below is mode-independent. self.device.cmd_bind_pipeline( cmd, vk::PipelineBindPoint::GRAPHICS, - self.graphics_pipeline, + self.pipelines[self.render_mode.index()], ); #[expect( @@ -592,6 +630,17 @@ impl Renderer { Ok(()) } + /// Sets the rasterisation mode used for subsequent frames. + pub const fn set_render_mode(&mut self, mode: RenderMode) { + self.render_mode = mode; + } + + /// Returns the rasterisation mode currently in use. + #[must_use] + pub const fn render_mode(&self) -> RenderMode { + self.render_mode + } + /// Frees the GPU mesh stored under `key`. Does nothing if no mesh is present. pub fn remove_mesh(&mut self, key: MeshKey) { let Some(mesh) = self.chunk_meshes.remove(&key) else { @@ -617,7 +666,10 @@ impl Drop for Renderer { unsafe { let _ = self.device.device_wait_idle(); - self.device.destroy_pipeline(self.graphics_pipeline, None); + // Every rasterisation variant is a distinct pipeline object and must be destroyed, or the validation layers report the survivors as leaked at teardown. + for pipeline in self.pipelines { + self.device.destroy_pipeline(pipeline, None); + } self.device .destroy_pipeline_layout(self.pipeline_layout, None);