From 97b1199b9e315c222827d6028eb157ed4c067ff7 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 18 Jul 2026 07:14:47 +0400 Subject: [PATCH] feat(render): accept static indexed Vulkan meshes --- adapters/fparkan-render-vulkan/src/ffi.rs | 9 +- .../src/ffi/resources.rs | 30 +++-- .../fparkan-render-vulkan/src/ffi/smoke.rs | 47 ++++---- .../src/ffi/smoke_types.rs | 110 ++++++++++++++++++ apps/fparkan-vulkan-smoke/src/main.rs | 3 +- 5 files changed, 158 insertions(+), 41 deletions(-) diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs index 0406544..0b6f1f2 100644 --- a/adapters/fparkan-render-vulkan/src/ffi.rs +++ b/adapters/fparkan-render-vulkan/src/ffi.rs @@ -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; diff --git a/adapters/fparkan-render-vulkan/src/ffi/resources.rs b/adapters/fparkan-render-vulkan/src/ffi/resources.rs index c6538d9..e5d725b 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/resources.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/resources.rs @@ -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 { - 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::()); - for vertex in vertices { - for value in vertex { + let mut bytes = Vec::with_capacity(mesh.vertices.len() * 5 * std::mem::size_of::()); + 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 { - let indices = [0_u16, 1_u16, 2_u16]; - let mut bytes = Vec::with_capacity(indices.len() * std::mem::size_of::()); - for index in indices { + let mut bytes = Vec::with_capacity(mesh.indices.len() * std::mem::size_of::()); + 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", ) } diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index afb5b2a..06f1182 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -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 { + 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,23 +157,25 @@ impl VulkanSmokeRenderer { progress.mark_swapchain_created(); } let command_pool = create_command_pool(&device)?; - let vertex_buffer = match create_triangle_vertex_buffer(&instance, &device) { - Ok(buffer) => buffer, - Err(error) => { - // SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure. - 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, - Err(error) => { - // SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure. - unsafe { device.device().destroy_command_pool(command_pool, None) }; - destroy_allocated_buffer(&device, &vertex_buffer); - return Err(error); - } - }; + 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. + unsafe { device.device().destroy_command_pool(command_pool, None) }; + return Err(error); + } + }; + 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. + unsafe { device.device().destroy_command_pool(command_pool, None) }; + destroy_allocated_buffer(&device, &vertex_buffer); + return Err(error); + } + }; let mut renderer = Self { instance: Some(instance), validation, @@ -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); } diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index a86de1b..31a2065 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -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>, } +/// 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, + /// Triangle-list indices into [`Self::vertices`]. + pub indices: Vec, +} + +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, pub(super) vertex_buffer: Option, pub(super) index_buffer: Option, + pub(super) index_count: u32, pub(super) frame_sync: Vec, pub(super) images_in_flight: Vec, pub(super) current_frame: usize, diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs index 9666b7b..88d7944 100644 --- a/apps/fparkan-vulkan-smoke/src/main.rs +++ b/apps/fparkan-vulkan-smoke/src/main.rs @@ -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,