feat(render): accept static indexed Vulkan meshes
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
Test / Lint (push) Failing after 2m6s
Test / Test (push) Skipped
Test / Render parity (push) Skipped

This commit is contained in:
2026-07-18 07:14:47 +04:00
parent 223b3b948a
commit 97b1199b9e
5 changed files with 158 additions and 41 deletions
+5 -4
View File
@@ -50,9 +50,9 @@ pub use self::instance::{
#[cfg(test)]
use self::instance::{cstring_vec, ensure_instance_extensions_available};
use self::resources::{
color_subresource_range, create_command_pool, create_frame_sync, create_triangle_index_buffer,
create_triangle_vertex_buffer, destroy_allocated_buffer, VulkanAllocatedBuffer,
VulkanFrameSync,
color_subresource_range, create_command_pool, create_frame_sync,
create_static_mesh_index_buffer, create_static_mesh_vertex_buffer, destroy_allocated_buffer,
VulkanAllocatedBuffer, VulkanFrameSync,
};
pub use self::runtime::{
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::{
VulkanSmokeBootstrapProgress, VulkanSmokeBootstrapSnapshot, VulkanSmokeFrameOutcome,
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError,
VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanValidationReport,
VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanStaticMesh, VulkanStaticVertex,
VulkanValidationReport,
};
#[cfg(test)]
use self::surface::extension_name;
@@ -2,7 +2,9 @@
use ash::vk;
use super::{VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeRendererError};
use super::{
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeRendererError, VulkanStaticMesh,
};
pub(super) struct VulkanAllocatedBuffer {
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,
device: &VulkanLogicalDeviceProbe,
mesh: &VulkanStaticMesh,
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
let vertices: [[f32; 5]; 3] = [
[0.0, -0.55, 1.0, 0.2, 0.2],
[0.55, 0.55, 0.2, 1.0, 0.2],
[-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 {
let mut bytes = Vec::with_capacity(mesh.vertices.len() * 5 * std::mem::size_of::<f32>());
for vertex in &mesh.vertices {
for value in vertex.position.into_iter().chain(vertex.color) {
bytes.extend_from_slice(&value.to_ne_bytes());
}
}
@@ -50,17 +48,17 @@ pub(super) fn create_triangle_vertex_buffer(
device,
&bytes,
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,
device: &VulkanLogicalDeviceProbe,
mesh: &VulkanStaticMesh,
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
let indices = [0_u16, 1_u16, 2_u16];
let mut bytes = Vec::with_capacity(indices.len() * std::mem::size_of::<u16>());
for index in indices {
let mut bytes = Vec::with_capacity(mesh.indices.len() * std::mem::size_of::<u16>());
for &index in &mesh.indices {
bytes.extend_from_slice(&index.to_ne_bytes());
}
create_host_visible_buffer(
@@ -68,7 +66,7 @@ pub(super) fn create_triangle_index_buffer(
device,
&bytes,
vk::BufferUsageFlags::INDEX_BUFFER,
"triangle index buffer",
"static mesh index buffer",
)
}
@@ -3,8 +3,8 @@
use ash::vk;
use super::{
create_command_pool, create_frame_sync, create_swapchain_resources,
create_triangle_index_buffer, create_triangle_vertex_buffer, create_validation_messenger,
create_command_pool, create_frame_sync, create_static_mesh_index_buffer,
create_static_mesh_vertex_buffer, create_swapchain_resources, create_validation_messenger,
create_vulkan_instance_probe, create_vulkan_logical_device_probe_for_request,
create_vulkan_surface_probe, create_vulkan_swapchain_probe_for_extent,
destroy_allocated_buffer, destroy_swapchain_resources, plan_vulkan_surface,
@@ -105,6 +105,10 @@ impl VulkanSmokeRenderer {
pub fn new(
create_info: &VulkanSmokeRendererCreateInfo,
) -> Result<Self, VulkanSmokeRendererError> {
create_info
.mesh
.validate()
.map_err(|context| VulkanSmokeRendererError::InvalidStaticMesh { context })?;
let bootstrap_progress = create_info.bootstrap_progress.as_ref();
let shader_manifest = validate_shader_manifest(&triangle_shader_manifest())
.map_err(VulkanSmokeRendererError::ShaderManifest)?;
@@ -153,7 +157,8 @@ impl VulkanSmokeRenderer {
progress.mark_swapchain_created();
}
let command_pool = create_command_pool(&device)?;
let vertex_buffer = match create_triangle_vertex_buffer(&instance, &device) {
let vertex_buffer =
match create_static_mesh_vertex_buffer(&instance, &device, &create_info.mesh) {
Ok(buffer) => buffer,
Err(error) => {
// SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
@@ -161,7 +166,8 @@ impl VulkanSmokeRenderer {
return Err(error);
}
};
let index_buffer = match create_triangle_index_buffer(&instance, &device) {
let index_buffer =
match create_static_mesh_index_buffer(&instance, &device, &create_info.mesh) {
Ok(buffer) => buffer,
Err(error) => {
// SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
@@ -180,6 +186,7 @@ impl VulkanSmokeRenderer {
swapchain_resources: None,
vertex_buffer: Some(vertex_buffer),
index_buffer: Some(index_buffer),
index_count: u32::try_from(create_info.mesh.indices.len()).unwrap_or(u32::MAX),
frame_sync: Vec::new(),
images_in_flight: Vec::new(),
current_frame: 0,
@@ -593,7 +600,7 @@ impl VulkanSmokeRenderer {
);
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);
}
@@ -24,10 +24,113 @@ pub struct VulkanSmokeRendererCreateInfo {
pub render_request: RenderRequest,
/// Whether validation layers must be enabled.
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.
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.
#[derive(Debug, Default)]
pub struct VulkanSmokeBootstrapProgress {
@@ -185,6 +288,11 @@ pub enum VulkanSmokeRendererError {
/// Operation context.
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.
InvariantViolation {
/// Missing state context.
@@ -206,6 +314,7 @@ impl std::fmt::Display for VulkanSmokeRendererError {
Self::MissingMemoryType { context } => {
write!(f, "{context}: no compatible Vulkan memory type")
}
Self::InvalidStaticMesh { context } => write!(f, "invalid static mesh: {context}"),
Self::InvariantViolation { context } => {
write!(f, "renderer invariant violated: {context}")
}
@@ -226,6 +335,7 @@ pub struct VulkanSmokeRenderer {
pub(super) swapchain_resources: Option<VulkanSwapchainResources>,
pub(super) vertex_buffer: Option<VulkanAllocatedBuffer>,
pub(super) index_buffer: Option<VulkanAllocatedBuffer>,
pub(super) index_count: u32,
pub(super) frame_sync: Vec<VulkanFrameSync>,
pub(super) images_in_flight: Vec<vk::Fence>,
pub(super) current_frame: usize,
+2 -1
View File
@@ -16,7 +16,7 @@ use fparkan_platform_winit::{window_native_handles, WinitWindowPlan};
use fparkan_render_vulkan::{
VulkanSmokeBootstrapProgress, VulkanSmokeFrameOutcome, VulkanSmokeRenderer,
VulkanSmokeRendererCreateInfo, VulkanSmokeRendererReport, VulkanSmokeShutdownReport,
VulkanValidationReport,
VulkanStaticMesh, VulkanValidationReport,
};
use serde::Serialize;
use std::path::PathBuf;
@@ -603,6 +603,7 @@ impl ApplicationHandler for SmokeApp {
drawable_extent: (size.width.max(1), size.height.max(1)),
render_request: RenderRequest::conservative(),
enable_validation: true,
mesh: VulkanStaticMesh::smoke_triangle(),
bootstrap_progress: Some(Arc::clone(&self.progress.bootstrap)),
}) {
Ok(renderer) => renderer,