feat(render): accept static indexed Vulkan meshes
This commit is contained in:
@@ -50,9 +50,9 @@ pub use self::instance::{
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use self::instance::{cstring_vec, ensure_instance_extensions_available};
|
use self::instance::{cstring_vec, ensure_instance_extensions_available};
|
||||||
use self::resources::{
|
use self::resources::{
|
||||||
color_subresource_range, create_command_pool, create_frame_sync, create_triangle_index_buffer,
|
color_subresource_range, create_command_pool, create_frame_sync,
|
||||||
create_triangle_vertex_buffer, destroy_allocated_buffer, VulkanAllocatedBuffer,
|
create_static_mesh_index_buffer, create_static_mesh_vertex_buffer, destroy_allocated_buffer,
|
||||||
VulkanFrameSync,
|
VulkanAllocatedBuffer, VulkanFrameSync,
|
||||||
};
|
};
|
||||||
pub use self::runtime::{
|
pub use self::runtime::{
|
||||||
create_vulkan_logical_device_probe, create_vulkan_logical_device_probe_for_request,
|
create_vulkan_logical_device_probe, create_vulkan_logical_device_probe_for_request,
|
||||||
@@ -61,7 +61,8 @@ pub use self::runtime::{
|
|||||||
pub use self::smoke_types::{
|
pub use self::smoke_types::{
|
||||||
VulkanSmokeBootstrapProgress, VulkanSmokeBootstrapSnapshot, VulkanSmokeFrameOutcome,
|
VulkanSmokeBootstrapProgress, VulkanSmokeBootstrapSnapshot, VulkanSmokeFrameOutcome,
|
||||||
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError,
|
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError,
|
||||||
VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanValidationReport,
|
VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanStaticMesh, VulkanStaticVertex,
|
||||||
|
VulkanValidationReport,
|
||||||
};
|
};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use self::surface::extension_name;
|
use self::surface::extension_name;
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
|
|
||||||
use super::{VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeRendererError};
|
use super::{
|
||||||
|
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeRendererError, VulkanStaticMesh,
|
||||||
|
};
|
||||||
|
|
||||||
pub(super) struct VulkanAllocatedBuffer {
|
pub(super) struct VulkanAllocatedBuffer {
|
||||||
pub(super) buffer: vk::Buffer,
|
pub(super) buffer: vk::Buffer,
|
||||||
@@ -30,18 +32,14 @@ pub(super) fn create_command_pool(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn create_triangle_vertex_buffer(
|
pub(super) fn create_static_mesh_vertex_buffer(
|
||||||
instance: &VulkanInstanceProbe,
|
instance: &VulkanInstanceProbe,
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
mesh: &VulkanStaticMesh,
|
||||||
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
||||||
let vertices: [[f32; 5]; 3] = [
|
let mut bytes = Vec::with_capacity(mesh.vertices.len() * 5 * std::mem::size_of::<f32>());
|
||||||
[0.0, -0.55, 1.0, 0.2, 0.2],
|
for vertex in &mesh.vertices {
|
||||||
[0.55, 0.55, 0.2, 1.0, 0.2],
|
for value in vertex.position.into_iter().chain(vertex.color) {
|
||||||
[-0.55, 0.55, 0.2, 0.4, 1.0],
|
|
||||||
];
|
|
||||||
let mut bytes = Vec::with_capacity(vertices.len() * 5 * std::mem::size_of::<f32>());
|
|
||||||
for vertex in vertices {
|
|
||||||
for value in vertex {
|
|
||||||
bytes.extend_from_slice(&value.to_ne_bytes());
|
bytes.extend_from_slice(&value.to_ne_bytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,17 +48,17 @@ pub(super) fn create_triangle_vertex_buffer(
|
|||||||
device,
|
device,
|
||||||
&bytes,
|
&bytes,
|
||||||
vk::BufferUsageFlags::VERTEX_BUFFER,
|
vk::BufferUsageFlags::VERTEX_BUFFER,
|
||||||
"triangle vertex buffer",
|
"static mesh vertex buffer",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn create_triangle_index_buffer(
|
pub(super) fn create_static_mesh_index_buffer(
|
||||||
instance: &VulkanInstanceProbe,
|
instance: &VulkanInstanceProbe,
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
mesh: &VulkanStaticMesh,
|
||||||
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
||||||
let indices = [0_u16, 1_u16, 2_u16];
|
let mut bytes = Vec::with_capacity(mesh.indices.len() * std::mem::size_of::<u16>());
|
||||||
let mut bytes = Vec::with_capacity(indices.len() * std::mem::size_of::<u16>());
|
for &index in &mesh.indices {
|
||||||
for index in indices {
|
|
||||||
bytes.extend_from_slice(&index.to_ne_bytes());
|
bytes.extend_from_slice(&index.to_ne_bytes());
|
||||||
}
|
}
|
||||||
create_host_visible_buffer(
|
create_host_visible_buffer(
|
||||||
@@ -68,7 +66,7 @@ pub(super) fn create_triangle_index_buffer(
|
|||||||
device,
|
device,
|
||||||
&bytes,
|
&bytes,
|
||||||
vk::BufferUsageFlags::INDEX_BUFFER,
|
vk::BufferUsageFlags::INDEX_BUFFER,
|
||||||
"triangle index buffer",
|
"static mesh index buffer",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
use ash::vk;
|
use ash::vk;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
create_command_pool, create_frame_sync, create_swapchain_resources,
|
create_command_pool, create_frame_sync, create_static_mesh_index_buffer,
|
||||||
create_triangle_index_buffer, create_triangle_vertex_buffer, create_validation_messenger,
|
create_static_mesh_vertex_buffer, create_swapchain_resources, create_validation_messenger,
|
||||||
create_vulkan_instance_probe, create_vulkan_logical_device_probe_for_request,
|
create_vulkan_instance_probe, create_vulkan_logical_device_probe_for_request,
|
||||||
create_vulkan_surface_probe, create_vulkan_swapchain_probe_for_extent,
|
create_vulkan_surface_probe, create_vulkan_swapchain_probe_for_extent,
|
||||||
destroy_allocated_buffer, destroy_swapchain_resources, plan_vulkan_surface,
|
destroy_allocated_buffer, destroy_swapchain_resources, plan_vulkan_surface,
|
||||||
@@ -105,6 +105,10 @@ impl VulkanSmokeRenderer {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
create_info: &VulkanSmokeRendererCreateInfo,
|
create_info: &VulkanSmokeRendererCreateInfo,
|
||||||
) -> Result<Self, VulkanSmokeRendererError> {
|
) -> Result<Self, VulkanSmokeRendererError> {
|
||||||
|
create_info
|
||||||
|
.mesh
|
||||||
|
.validate()
|
||||||
|
.map_err(|context| VulkanSmokeRendererError::InvalidStaticMesh { context })?;
|
||||||
let bootstrap_progress = create_info.bootstrap_progress.as_ref();
|
let bootstrap_progress = create_info.bootstrap_progress.as_ref();
|
||||||
let shader_manifest = validate_shader_manifest(&triangle_shader_manifest())
|
let shader_manifest = validate_shader_manifest(&triangle_shader_manifest())
|
||||||
.map_err(VulkanSmokeRendererError::ShaderManifest)?;
|
.map_err(VulkanSmokeRendererError::ShaderManifest)?;
|
||||||
@@ -153,23 +157,25 @@ impl VulkanSmokeRenderer {
|
|||||||
progress.mark_swapchain_created();
|
progress.mark_swapchain_created();
|
||||||
}
|
}
|
||||||
let command_pool = create_command_pool(&device)?;
|
let command_pool = create_command_pool(&device)?;
|
||||||
let vertex_buffer = match create_triangle_vertex_buffer(&instance, &device) {
|
let vertex_buffer =
|
||||||
Ok(buffer) => buffer,
|
match create_static_mesh_vertex_buffer(&instance, &device, &create_info.mesh) {
|
||||||
Err(error) => {
|
Ok(buffer) => buffer,
|
||||||
// SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
|
Err(error) => {
|
||||||
unsafe { device.device().destroy_command_pool(command_pool, None) };
|
// SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
|
||||||
return Err(error);
|
unsafe { device.device().destroy_command_pool(command_pool, None) };
|
||||||
}
|
return Err(error);
|
||||||
};
|
}
|
||||||
let index_buffer = match create_triangle_index_buffer(&instance, &device) {
|
};
|
||||||
Ok(buffer) => buffer,
|
let index_buffer =
|
||||||
Err(error) => {
|
match create_static_mesh_index_buffer(&instance, &device, &create_info.mesh) {
|
||||||
// SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
|
Ok(buffer) => buffer,
|
||||||
unsafe { device.device().destroy_command_pool(command_pool, None) };
|
Err(error) => {
|
||||||
destroy_allocated_buffer(&device, &vertex_buffer);
|
// SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
|
||||||
return Err(error);
|
unsafe { device.device().destroy_command_pool(command_pool, None) };
|
||||||
}
|
destroy_allocated_buffer(&device, &vertex_buffer);
|
||||||
};
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
let mut renderer = Self {
|
let mut renderer = Self {
|
||||||
instance: Some(instance),
|
instance: Some(instance),
|
||||||
validation,
|
validation,
|
||||||
@@ -180,6 +186,7 @@ impl VulkanSmokeRenderer {
|
|||||||
swapchain_resources: None,
|
swapchain_resources: None,
|
||||||
vertex_buffer: Some(vertex_buffer),
|
vertex_buffer: Some(vertex_buffer),
|
||||||
index_buffer: Some(index_buffer),
|
index_buffer: Some(index_buffer),
|
||||||
|
index_count: u32::try_from(create_info.mesh.indices.len()).unwrap_or(u32::MAX),
|
||||||
frame_sync: Vec::new(),
|
frame_sync: Vec::new(),
|
||||||
images_in_flight: Vec::new(),
|
images_in_flight: Vec::new(),
|
||||||
current_frame: 0,
|
current_frame: 0,
|
||||||
@@ -593,7 +600,7 @@ impl VulkanSmokeRenderer {
|
|||||||
);
|
);
|
||||||
device
|
device
|
||||||
.device()
|
.device()
|
||||||
.cmd_draw_indexed(command_buffer, 3, 1, 0, 0, 0);
|
.cmd_draw_indexed(command_buffer, self.index_count, 1, 0, 0, 0);
|
||||||
device.device().cmd_end_render_pass(command_buffer);
|
device.device().cmd_end_render_pass(command_buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,113 @@ pub struct VulkanSmokeRendererCreateInfo {
|
|||||||
pub render_request: RenderRequest,
|
pub render_request: RenderRequest,
|
||||||
/// Whether validation layers must be enabled.
|
/// Whether validation layers must be enabled.
|
||||||
pub enable_validation: bool,
|
pub enable_validation: bool,
|
||||||
|
/// Static indexed geometry uploaded before the first live frame.
|
||||||
|
///
|
||||||
|
/// This initial bridge keeps positions in clip-space. MSH transforms,
|
||||||
|
/// materials, and textures are deliberately higher-level Stage 3 work.
|
||||||
|
pub mesh: VulkanStaticMesh,
|
||||||
/// Optional shared bootstrap progress tracker for failure evidence.
|
/// Optional shared bootstrap progress tracker for failure evidence.
|
||||||
pub bootstrap_progress: Option<Arc<VulkanSmokeBootstrapProgress>>,
|
pub bootstrap_progress: Option<Arc<VulkanSmokeBootstrapProgress>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One vertex accepted by the initial static Vulkan geometry path.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct VulkanStaticVertex {
|
||||||
|
/// Position in Vulkan clip-space XY coordinates.
|
||||||
|
pub position: [f32; 2],
|
||||||
|
/// Linear RGB vertex color.
|
||||||
|
pub color: [f32; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Static indexed geometry uploaded to live Vulkan buffers.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct VulkanStaticMesh {
|
||||||
|
/// Vertex data in pipeline order.
|
||||||
|
pub vertices: Vec<VulkanStaticVertex>,
|
||||||
|
/// Triangle-list indices into [`Self::vertices`].
|
||||||
|
pub indices: Vec<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VulkanStaticMesh {
|
||||||
|
/// Returns the compatibility triangle used by the native Stage 0 smoke app.
|
||||||
|
#[must_use]
|
||||||
|
pub fn smoke_triangle() -> Self {
|
||||||
|
Self {
|
||||||
|
vertices: vec![
|
||||||
|
VulkanStaticVertex {
|
||||||
|
position: [0.0, -0.55],
|
||||||
|
color: [1.0, 0.2, 0.2],
|
||||||
|
},
|
||||||
|
VulkanStaticVertex {
|
||||||
|
position: [0.55, 0.55],
|
||||||
|
color: [0.2, 1.0, 0.2],
|
||||||
|
},
|
||||||
|
VulkanStaticVertex {
|
||||||
|
position: [-0.55, 0.55],
|
||||||
|
color: [0.2, 0.4, 1.0],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indices: vec![0, 1, 2],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn validate(&self) -> Result<(), &'static str> {
|
||||||
|
if self.vertices.is_empty() {
|
||||||
|
return Err("static mesh has no vertices");
|
||||||
|
}
|
||||||
|
if self.indices.is_empty() || !self.indices.len().is_multiple_of(3) {
|
||||||
|
return Err("static mesh indices must contain complete triangles");
|
||||||
|
}
|
||||||
|
if self
|
||||||
|
.indices
|
||||||
|
.iter()
|
||||||
|
.any(|&index| usize::from(index) >= self.vertices.len())
|
||||||
|
{
|
||||||
|
return Err("static mesh index exceeds vertex count");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod static_mesh_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn smoke_triangle_is_valid_complete_geometry() {
|
||||||
|
let mesh = VulkanStaticMesh::smoke_triangle();
|
||||||
|
|
||||||
|
assert_eq!(mesh.indices, vec![0, 1, 2]);
|
||||||
|
assert_eq!(mesh.validate(), Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn static_mesh_rejects_bad_triangle_topology_and_indices() {
|
||||||
|
let no_vertices = VulkanStaticMesh {
|
||||||
|
vertices: Vec::new(),
|
||||||
|
indices: vec![0, 1, 2],
|
||||||
|
};
|
||||||
|
let incomplete_triangle = VulkanStaticMesh {
|
||||||
|
vertices: VulkanStaticMesh::smoke_triangle().vertices,
|
||||||
|
indices: vec![0, 1],
|
||||||
|
};
|
||||||
|
let out_of_range_index = VulkanStaticMesh {
|
||||||
|
vertices: VulkanStaticMesh::smoke_triangle().vertices,
|
||||||
|
indices: vec![0, 1, 3],
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(no_vertices.validate(), Err("static mesh has no vertices"));
|
||||||
|
assert_eq!(
|
||||||
|
incomplete_triangle.validate(),
|
||||||
|
Err("static mesh indices must contain complete triangles")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
out_of_range_index.validate(),
|
||||||
|
Err("static mesh index exceeds vertex count")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Shared bootstrap progress used to report partial renderer startup evidence.
|
/// Shared bootstrap progress used to report partial renderer startup evidence.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct VulkanSmokeBootstrapProgress {
|
pub struct VulkanSmokeBootstrapProgress {
|
||||||
@@ -185,6 +288,11 @@ pub enum VulkanSmokeRendererError {
|
|||||||
/// Operation context.
|
/// Operation context.
|
||||||
context: &'static str,
|
context: &'static str,
|
||||||
},
|
},
|
||||||
|
/// The submitted static geometry cannot be represented by this path.
|
||||||
|
InvalidStaticMesh {
|
||||||
|
/// Validation failure detail.
|
||||||
|
context: &'static str,
|
||||||
|
},
|
||||||
/// Internal smoke renderer state was unexpectedly absent.
|
/// Internal smoke renderer state was unexpectedly absent.
|
||||||
InvariantViolation {
|
InvariantViolation {
|
||||||
/// Missing state context.
|
/// Missing state context.
|
||||||
@@ -206,6 +314,7 @@ impl std::fmt::Display for VulkanSmokeRendererError {
|
|||||||
Self::MissingMemoryType { context } => {
|
Self::MissingMemoryType { context } => {
|
||||||
write!(f, "{context}: no compatible Vulkan memory type")
|
write!(f, "{context}: no compatible Vulkan memory type")
|
||||||
}
|
}
|
||||||
|
Self::InvalidStaticMesh { context } => write!(f, "invalid static mesh: {context}"),
|
||||||
Self::InvariantViolation { context } => {
|
Self::InvariantViolation { context } => {
|
||||||
write!(f, "renderer invariant violated: {context}")
|
write!(f, "renderer invariant violated: {context}")
|
||||||
}
|
}
|
||||||
@@ -226,6 +335,7 @@ pub struct VulkanSmokeRenderer {
|
|||||||
pub(super) swapchain_resources: Option<VulkanSwapchainResources>,
|
pub(super) swapchain_resources: Option<VulkanSwapchainResources>,
|
||||||
pub(super) vertex_buffer: Option<VulkanAllocatedBuffer>,
|
pub(super) vertex_buffer: Option<VulkanAllocatedBuffer>,
|
||||||
pub(super) index_buffer: Option<VulkanAllocatedBuffer>,
|
pub(super) index_buffer: Option<VulkanAllocatedBuffer>,
|
||||||
|
pub(super) index_count: u32,
|
||||||
pub(super) frame_sync: Vec<VulkanFrameSync>,
|
pub(super) frame_sync: Vec<VulkanFrameSync>,
|
||||||
pub(super) images_in_flight: Vec<vk::Fence>,
|
pub(super) images_in_flight: Vec<vk::Fence>,
|
||||||
pub(super) current_frame: usize,
|
pub(super) current_frame: usize,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use fparkan_platform_winit::{window_native_handles, WinitWindowPlan};
|
|||||||
use fparkan_render_vulkan::{
|
use fparkan_render_vulkan::{
|
||||||
VulkanSmokeBootstrapProgress, VulkanSmokeFrameOutcome, VulkanSmokeRenderer,
|
VulkanSmokeBootstrapProgress, VulkanSmokeFrameOutcome, VulkanSmokeRenderer,
|
||||||
VulkanSmokeRendererCreateInfo, VulkanSmokeRendererReport, VulkanSmokeShutdownReport,
|
VulkanSmokeRendererCreateInfo, VulkanSmokeRendererReport, VulkanSmokeShutdownReport,
|
||||||
VulkanValidationReport,
|
VulkanStaticMesh, VulkanValidationReport,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -603,6 +603,7 @@ impl ApplicationHandler for SmokeApp {
|
|||||||
drawable_extent: (size.width.max(1), size.height.max(1)),
|
drawable_extent: (size.width.max(1), size.height.max(1)),
|
||||||
render_request: RenderRequest::conservative(),
|
render_request: RenderRequest::conservative(),
|
||||||
enable_validation: true,
|
enable_validation: true,
|
||||||
|
mesh: VulkanStaticMesh::smoke_triangle(),
|
||||||
bootstrap_progress: Some(Arc::clone(&self.progress.bootstrap)),
|
bootstrap_progress: Some(Arc::clone(&self.progress.bootstrap)),
|
||||||
}) {
|
}) {
|
||||||
Ok(renderer) => renderer,
|
Ok(renderer) => renderer,
|
||||||
|
|||||||
Reference in New Issue
Block a user