feat(renderer): compose render modes from raster passes
This commit is contained in:
parent
8af6ae9661
commit
324d1b144d
|
|
@ -53,8 +53,10 @@ pub fn create_logical_device(
|
||||||
let mut dynamic_rendering_features =
|
let mut dynamic_rendering_features =
|
||||||
vk::PhysicalDeviceDynamicRenderingFeatures::default().dynamic_rendering(true);
|
vk::PhysicalDeviceDynamicRenderingFeatures::default().dynamic_rendering(true);
|
||||||
|
|
||||||
// `fillModeNonSolid` unlocks the `POINT` and `LINE` polygon modes used by the debug render modes.
|
// `fillModeNonSolid` unlocks the `POINT` and `LINE` polygon modes used by the debug render modes. `largePoints` permits a shader-written point size above 1.0, without which debug points rasterise as single pixels.
|
||||||
let enabled_features = vk::PhysicalDeviceFeatures::default().fill_mode_non_solid(true);
|
let enabled_features = vk::PhysicalDeviceFeatures::default()
|
||||||
|
.fill_mode_non_solid(true)
|
||||||
|
.large_points(true);
|
||||||
|
|
||||||
let create_info = vk::DeviceCreateInfo::default()
|
let create_info = vk::DeviceCreateInfo::default()
|
||||||
.queue_create_infos(std::slice::from_ref(&queue_info))
|
.queue_create_infos(std::slice::from_ref(&queue_info))
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
|
||||||
use std::ffi::c_char;
|
use std::ffi::c_char;
|
||||||
|
|
||||||
pub use error::RendererError;
|
pub use error::RendererError;
|
||||||
pub use renderer::{MeshKey, RenderMode, Renderer};
|
pub use renderer::{MeshKey, RasterPass, RenderMode, Renderer};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|
@ -125,14 +125,15 @@ impl Renderer {
|
||||||
// 12. Graphics Pipeline Configuration
|
// 12. Graphics Pipeline Configuration
|
||||||
let pipeline_layout = pipeline::create_pipeline_layout(&device)?;
|
let pipeline_layout = pipeline::create_pipeline_layout(&device)?;
|
||||||
|
|
||||||
// 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.
|
// One pipeline per raster pass, built up front so selecting a mode is a bind-time choice rather than a pipeline compilation stall. All variants share `pipeline_layout`; only their rasterisation and depth-compare state differs.
|
||||||
let mut pipelines = [vk::Pipeline::null(); RenderMode::COUNT];
|
let mut pipelines = [vk::Pipeline::null(); RasterPass::COUNT];
|
||||||
for mode in RenderMode::ALL {
|
for pass in RasterPass::ALL {
|
||||||
pipelines[mode.index()] = pipeline::create_graphics_pipeline(
|
pipelines[pass.index()] = pipeline::create_graphics_pipeline(
|
||||||
&device,
|
&device,
|
||||||
pipeline_layout,
|
pipeline_layout,
|
||||||
swapchain_format,
|
swapchain_format,
|
||||||
mode.polygon_mode(),
|
pass.polygon_mode(),
|
||||||
|
pass.depth_compare_op(),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ pub fn create_graphics_pipeline(
|
||||||
layout: vk::PipelineLayout,
|
layout: vk::PipelineLayout,
|
||||||
color_format: vk::Format,
|
color_format: vk::Format,
|
||||||
polygon_mode: vk::PolygonMode,
|
polygon_mode: vk::PolygonMode,
|
||||||
|
depth_compare_op: vk::CompareOp,
|
||||||
) -> Result<vk::Pipeline, RendererError> {
|
) -> Result<vk::Pipeline, RendererError> {
|
||||||
// 1. Load and compile shader modules
|
// 1. Load and compile shader modules
|
||||||
let (vert_module, frag_module) = load_shader_modules(device)?;
|
let (vert_module, frag_module) = load_shader_modules(device)?;
|
||||||
|
|
@ -132,7 +133,7 @@ pub fn create_graphics_pipeline(
|
||||||
let depth_stencil_state = &vk::PipelineDepthStencilStateCreateInfo::default()
|
let depth_stencil_state = &vk::PipelineDepthStencilStateCreateInfo::default()
|
||||||
.depth_test_enable(true)
|
.depth_test_enable(true)
|
||||||
.depth_write_enable(true)
|
.depth_write_enable(true)
|
||||||
.depth_compare_op(vk::CompareOp::LESS)
|
.depth_compare_op(depth_compare_op)
|
||||||
.depth_bounds_test_enable(false)
|
.depth_bounds_test_enable(false)
|
||||||
.stencil_test_enable(false);
|
.stencil_test_enable(false);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,41 +10,101 @@ use std::collections::HashMap;
|
||||||
/// Opaque, renderer-side identifier for one uploaded chunk mesh.
|
/// Opaque, renderer-side identifier for one uploaded chunk mesh.
|
||||||
pub type MeshKey = (i32, i32, i32);
|
pub type MeshKey = (i32, i32, i32);
|
||||||
|
|
||||||
/// Selects how chunk meshes are rasterised.
|
/// One rasterisation pass over the visible chunk meshes.
|
||||||
///
|
///
|
||||||
/// 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`].
|
/// A pass corresponds one-to-one with a pipeline object, since polygon mode and depth-compare state are baked into a pipeline and cannot be changed by a command. Passes are the GPU-level primitive; [`RenderMode`] composes them into what is actually presented.
|
||||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
|
///
|
||||||
pub enum RenderMode {
|
/// Adding a pass requires three compiler-checked edits: the variant, an entry in [`RasterPass::ALL`] (whose length is pinned to [`RasterPass::COUNT`]), and arms in the `match`es below.
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub enum RasterPass {
|
||||||
/// Filled triangles; the normal presentation path.
|
/// Filled triangles; the normal presentation path.
|
||||||
#[default]
|
Fill,
|
||||||
Filled,
|
|
||||||
/// One point per polygon vertex, exposing the density of the geometry the mesher emitted.
|
/// One point per polygon vertex, exposing the density of the geometry the mesher emitted.
|
||||||
Points,
|
Points,
|
||||||
|
/// Triangle edges only, exposing the shape and size of the quads the mesher produced.
|
||||||
|
Wireframe,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderMode {
|
impl RasterPass {
|
||||||
/// Number of variants, and therefore the number of pipelines built at initialisation.
|
/// Number of passes, and therefore the number of pipelines built at initialisation.
|
||||||
pub const COUNT: usize = 2;
|
pub const COUNT: usize = 3;
|
||||||
|
|
||||||
/// 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.
|
/// Every pass, in discriminant order. The array length is checked against [`RasterPass::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];
|
pub const ALL: [Self; Self::COUNT] = [Self::Fill, Self::Points, Self::Wireframe];
|
||||||
|
|
||||||
/// Returns the rasterisation polygon mode backing this render mode.
|
/// Returns the rasterisation polygon mode backing this pass.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn polygon_mode(self) -> vk::PolygonMode {
|
pub const fn polygon_mode(self) -> vk::PolygonMode {
|
||||||
match self {
|
match self {
|
||||||
Self::Filled => vk::PolygonMode::FILL,
|
Self::Fill => vk::PolygonMode::FILL,
|
||||||
Self::Points => vk::PolygonMode::POINT,
|
Self::Points => vk::PolygonMode::POINT,
|
||||||
|
Self::Wireframe => vk::PolygonMode::LINE,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns this mode's position in [`RenderMode::ALL`], used to index [`Renderer::pipelines`].
|
/// Returns the depth-comparison used by this pass.
|
||||||
|
///
|
||||||
|
/// Debug passes use `LESS_OR_EQUAL` so they survive being drawn over a filled pass that has already written the same depth values; a strict `LESS` would reject every overlaid fragment and render the overlay invisible.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn depth_compare_op(self) -> vk::CompareOp {
|
||||||
|
match self {
|
||||||
|
Self::Fill => vk::CompareOp::LESS,
|
||||||
|
Self::Points | Self::Wireframe => vk::CompareOp::LESS_OR_EQUAL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the debug-tint weight pushed to the vertex shader for this pass.
|
||||||
|
///
|
||||||
|
/// Debug passes are tinted a uniform colour because geometry drawn in the terrain's own vertex colours is indistinguishable from the surface beneath it when overlaid.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn tint(self) -> f32 {
|
||||||
|
match self {
|
||||||
|
Self::Fill => 0.0,
|
||||||
|
Self::Points | Self::Wireframe => 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns this pass's position in [`RasterPass::ALL`], used to index [`Renderer::pipelines`].
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn index(self) -> usize {
|
pub const fn index(self) -> usize {
|
||||||
self as usize
|
self as usize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Selects how chunk meshes are presented, as an ordered list of [`RasterPass`]es.
|
||||||
|
///
|
||||||
|
/// Non-[`RenderMode::Filled`] variants are debug modes for inspecting the mesher's output; they are not gameplay state. The `Filled*` variants draw the terrain normally and overlay a debug pass on top, which keeps the surface readable while showing where its geometry actually lies.
|
||||||
|
///
|
||||||
|
/// Adding a mode is one variant plus one arm in [`RenderMode::passes`]; it needs a new [`RasterPass`] only if it requires rasterisation state that no existing pass provides.
|
||||||
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||||
|
pub enum RenderMode {
|
||||||
|
/// Filled triangles only; the normal presentation path.
|
||||||
|
#[default]
|
||||||
|
Filled,
|
||||||
|
/// Vertex points only, against the clear colour.
|
||||||
|
Points,
|
||||||
|
/// Triangle edges only, against the clear colour.
|
||||||
|
Wireframe,
|
||||||
|
/// Filled triangles with vertex points overlaid.
|
||||||
|
FilledPoints,
|
||||||
|
/// Filled triangles with triangle edges overlaid.
|
||||||
|
FilledWireframe,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderMode {
|
||||||
|
/// Returns the passes to run, in submission order. Later passes are drawn over earlier ones.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn passes(self) -> &'static [RasterPass] {
|
||||||
|
match self {
|
||||||
|
Self::Filled => &[RasterPass::Fill],
|
||||||
|
Self::Points => &[RasterPass::Points],
|
||||||
|
Self::Wireframe => &[RasterPass::Wireframe],
|
||||||
|
Self::FilledPoints => &[RasterPass::Fill, RasterPass::Points],
|
||||||
|
Self::FilledWireframe => &[RasterPass::Fill, RasterPass::Wireframe],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// GPU resources for a single chunk mesh, drawn at a fixed world offset.
|
/// GPU resources for a single chunk mesh, drawn at a fixed world offset.
|
||||||
pub(crate) struct GpuMesh {
|
pub(crate) struct GpuMesh {
|
||||||
/// Buffer holding the chunk's vertex data.
|
/// Buffer holding the chunk's vertex data.
|
||||||
|
|
@ -105,8 +165,8 @@ pub struct Renderer {
|
||||||
pub(crate) command_buffers: Vec<vk::CommandBuffer>,
|
pub(crate) command_buffers: Vec<vk::CommandBuffer>,
|
||||||
/// The layout of the graphics pipeline.
|
/// The layout of the graphics pipeline.
|
||||||
pub(crate) pipeline_layout: vk::PipelineLayout,
|
pub(crate) pipeline_layout: vk::PipelineLayout,
|
||||||
/// One compiled pipeline per [`RenderMode`], indexed by [`RenderMode::index`]. All variants share [`Renderer::pipeline_layout`] and differ only in rasterisation state.
|
/// One compiled pipeline per [`RasterPass`], indexed by [`RasterPass::index`]. All variants share [`Renderer::pipeline_layout`] and differ only in rasterisation and depth-compare state.
|
||||||
pub(crate) pipelines: [vk::Pipeline; RenderMode::COUNT],
|
pub(crate) pipelines: [vk::Pipeline; RasterPass::COUNT],
|
||||||
/// The rasterisation mode selected for subsequent frames, chosen by [`Renderer::set_render_mode`].
|
/// The rasterisation mode selected for subsequent frames, chosen by [`Renderer::set_render_mode`].
|
||||||
pub(crate) render_mode: RenderMode,
|
pub(crate) render_mode: RenderMode,
|
||||||
/// Memory manager for GPU allocations.
|
/// Memory manager for GPU allocations.
|
||||||
|
|
@ -422,13 +482,6 @@ impl Renderer {
|
||||||
/// Issues the actual draw calls for the frame.
|
/// Issues the actual draw calls for the frame.
|
||||||
fn issue_draw_calls(&self, cmd: vk::CommandBuffer, camera_view: glam::Mat4) {
|
fn issue_draw_calls(&self, cmd: vk::CommandBuffer, camera_view: glam::Mat4) {
|
||||||
unsafe {
|
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.pipelines[self.render_mode.index()],
|
|
||||||
);
|
|
||||||
|
|
||||||
#[expect(
|
#[expect(
|
||||||
clippy::cast_precision_loss,
|
clippy::cast_precision_loss,
|
||||||
reason = "swapchain extents are within f32's exact-integer range"
|
reason = "swapchain extents are within f32's exact-integer range"
|
||||||
|
|
@ -475,9 +528,27 @@ impl Renderer {
|
||||||
reason = "CHUNK_SIZE is 32, exactly representable as f32"
|
reason = "CHUNK_SIZE is 32, exactly representable as f32"
|
||||||
)]
|
)]
|
||||||
let chunk_extent = glam::Vec3::splat(shared::world::CHUNK_SIZE as f32);
|
let chunk_extent = glam::Vec3::splat(shared::world::CHUNK_SIZE as f32);
|
||||||
|
// Culling is performed once per frame rather than once per pass: the frustum does not change between passes, so the surviving set is shared by all of them.
|
||||||
let mut culled: u32 = 0;
|
let mut culled: u32 = 0;
|
||||||
|
let visible: Vec<&GpuMesh> = self
|
||||||
|
.chunk_meshes
|
||||||
|
.values()
|
||||||
|
.filter(|mesh| {
|
||||||
|
// Reject the chunk when its world-space bounding box falls entirely outside the frustum.
|
||||||
|
let box_min = glam::Vec3::from(mesh.world_offset) - glam::Vec3::splat(0.5);
|
||||||
|
let visible = frustum.intersects_aabb(box_min, box_min + chunk_extent);
|
||||||
|
if !visible {
|
||||||
|
culled += 1;
|
||||||
|
}
|
||||||
|
visible
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
// The MVP is identical for every chunk this frame, so it is pushed once before the loop.
|
if culled > 0 {
|
||||||
|
tracing::debug!(culled, "chunks skipped by frustum culling");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The MVP is identical for every chunk and every pass this frame, so it is pushed once before the loops.
|
||||||
let mvp_bytes = bytemuck::cast_slice(mvp.as_ref());
|
let mvp_bytes = bytemuck::cast_slice(mvp.as_ref());
|
||||||
self.device.cmd_push_constants(
|
self.device.cmd_push_constants(
|
||||||
cmd,
|
cmd,
|
||||||
|
|
@ -494,20 +565,21 @@ impl Renderer {
|
||||||
)]
|
)]
|
||||||
let chunk_offset_byte = size_of::<glam::Mat4>() as u32;
|
let chunk_offset_byte = size_of::<glam::Mat4>() as u32;
|
||||||
|
|
||||||
for mesh in self.chunk_meshes.values() {
|
// Overlay modes submit the same geometry more than once, each pass binding a pipeline whose rasterisation state differs. Later passes draw over earlier ones.
|
||||||
// Reject the chunk when its world-space bounding box falls entirely outside the frustum.
|
for pass in self.render_mode.passes() {
|
||||||
let box_min = glam::Vec3::from(mesh.world_offset) - glam::Vec3::splat(0.5);
|
self.device.cmd_bind_pipeline(
|
||||||
if !frustum.intersects_aabb(box_min, box_min + chunk_extent) {
|
cmd,
|
||||||
culled += 1;
|
vk::PipelineBindPoint::GRAPHICS,
|
||||||
continue;
|
self.pipelines[pass.index()],
|
||||||
}
|
);
|
||||||
|
|
||||||
// The offset is padded to a vec4 to match the std140 layout of the push-constant block; only xyz is read by the shader.
|
for mesh in &visible {
|
||||||
|
// The offset is padded to a vec4 to match the std140 layout of the push-constant block. The shader reads xyz as the chunk's world offset and w as the debug-tint weight for this pass.
|
||||||
let offset = [
|
let offset = [
|
||||||
mesh.world_offset[0],
|
mesh.world_offset[0],
|
||||||
mesh.world_offset[1],
|
mesh.world_offset[1],
|
||||||
mesh.world_offset[2],
|
mesh.world_offset[2],
|
||||||
0.0_f32,
|
pass.tint(),
|
||||||
];
|
];
|
||||||
self.device.cmd_push_constants(
|
self.device.cmd_push_constants(
|
||||||
cmd,
|
cmd,
|
||||||
|
|
@ -519,14 +591,15 @@ impl Renderer {
|
||||||
|
|
||||||
self.device
|
self.device
|
||||||
.cmd_bind_vertex_buffers(cmd, 0, &[mesh.vertex_buffer], &[0]);
|
.cmd_bind_vertex_buffers(cmd, 0, &[mesh.vertex_buffer], &[0]);
|
||||||
self.device
|
self.device.cmd_bind_index_buffer(
|
||||||
.cmd_bind_index_buffer(cmd, mesh.index_buffer, 0, vk::IndexType::UINT32);
|
cmd,
|
||||||
|
mesh.index_buffer,
|
||||||
|
0,
|
||||||
|
vk::IndexType::UINT32,
|
||||||
|
);
|
||||||
self.device
|
self.device
|
||||||
.cmd_draw_indexed(cmd, mesh.index_count, 1, 0, 0, 0);
|
.cmd_draw_indexed(cmd, mesh.index_count, 1, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if culled > 0 {
|
|
||||||
tracing::debug!(culled, "chunks skipped by frustum culling");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue