aboutsummaryrefslogtreecommitdiff
path: root/adapters
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-07-18 06:21:29 +0300
committerValentin Popov <valentin@popov.link>2026-07-18 06:21:29 +0300
commit283b48e8c512dd251d4617cc988985e3be2a0c5e (patch)
tree4634548bcb794ac6d1ff514ea1fd63f7350d436e /adapters
parent97b1199b9e315c222827d6028eb157ed4c067ff7 (diff)
downloadfparkan-283b48e8c512dd251d4617cc988985e3be2a0c5e.tar.xz
fparkan-283b48e8c512dd251d4617cc988985e3be2a0c5e.zip
feat(render): draw original MSH through Vulkan
Diffstat (limited to 'adapters')
-rw-r--r--adapters/fparkan-render-vulkan/Cargo.toml1
-rw-r--r--adapters/fparkan-render-vulkan/src/asset_mesh.rs161
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi.rs3
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs4
4 files changed, 168 insertions, 1 deletions
diff --git a/adapters/fparkan-render-vulkan/Cargo.toml b/adapters/fparkan-render-vulkan/Cargo.toml
index bb7dbd0..909ac1f 100644
--- a/adapters/fparkan-render-vulkan/Cargo.toml
+++ b/adapters/fparkan-render-vulkan/Cargo.toml
@@ -10,6 +10,7 @@ build = "build.rs"
ash = "0.38"
ash-window = "0.13"
fparkan-binary = { path = "../../crates/fparkan-binary", version = "0.1.0" }
+fparkan-msh = { path = "../../crates/fparkan-msh", version = "0.1.0" }
fparkan-platform = { path = "../../crates/fparkan-platform", version = "0.1.0" }
fparkan-render = { path = "../../crates/fparkan-render", version = "0.1.0" }
serde = { version = "1.0", features = ["derive"] }
diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs
new file mode 100644
index 0000000..8e45f08
--- /dev/null
+++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs
@@ -0,0 +1,161 @@
+//! Format-to-GPU geometry bridge for the initial static asset renderer.
+
+use crate::{VulkanStaticMesh, VulkanStaticVertex};
+use fparkan_msh::ModelAsset;
+
+/// Error returned when a validated MSH cannot enter the current static GPU path.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum VulkanAssetMeshError {
+ /// The model has no triangle batches.
+ EmptyGeometry,
+ /// A position used by a batch is non-finite.
+ NonFinitePosition,
+ /// The view-plane extent is zero or otherwise not usable for normalization.
+ DegenerateViewExtent,
+ /// The indexed model exceeds the current 16-bit Vulkan input contract.
+ IndexOutOfRange,
+}
+
+impl std::fmt::Display for VulkanAssetMeshError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::EmptyGeometry => write!(f, "MSH contains no triangle geometry"),
+ Self::NonFinitePosition => write!(f, "MSH contains a non-finite position"),
+ Self::DegenerateViewExtent => write!(f, "MSH has a degenerate XZ view extent"),
+ Self::IndexOutOfRange => write!(f, "MSH index exceeds the static Vulkan u16 contract"),
+ }
+ }
+}
+
+impl std::error::Error for VulkanAssetMeshError {}
+
+/// Projects validated MSH geometry into the current static Vulkan clip-space path.
+///
+/// The original engine's node transforms, camera and material pipeline are not
+/// substituted here. This bridge is intentionally a static asset-viewer step:
+/// it preserves every batch's `index_start`, `index_count` and `base_vertex`,
+/// projects the conventional `Iron3D` XZ ground plane into the current XY shader
+/// input, and scales the used bounds uniformly into the visible clip rectangle.
+///
+/// # Errors
+///
+/// Returns [`VulkanAssetMeshError`] if the source cannot be represented by the
+/// current 16-bit, triangle-list viewer input.
+pub fn project_msh_to_static_mesh(
+ model: &ModelAsset,
+) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
+ let mut indices = Vec::new();
+ for batch in &model.batches {
+ let start = usize::try_from(batch.index_start)
+ .map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?;
+ let end = start
+ .checked_add(usize::from(batch.index_count))
+ .ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
+ for &raw_index in model
+ .indices
+ .get(start..end)
+ .ok_or(VulkanAssetMeshError::IndexOutOfRange)?
+ {
+ let index = batch
+ .base_vertex
+ .checked_add(u32::from(raw_index))
+ .ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
+ indices.push(u16::try_from(index).map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?);
+ }
+ }
+ if indices.is_empty() {
+ return Err(VulkanAssetMeshError::EmptyGeometry);
+ }
+
+ let mut min_x = f32::INFINITY;
+ let mut max_x = f32::NEG_INFINITY;
+ let mut min_z = f32::INFINITY;
+ let mut max_z = f32::NEG_INFINITY;
+ for &index in &indices {
+ let position = model
+ .positions
+ .get(usize::from(index))
+ .ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
+ if !position.iter().all(|value| value.is_finite()) {
+ return Err(VulkanAssetMeshError::NonFinitePosition);
+ }
+ min_x = min_x.min(position[0]);
+ max_x = max_x.max(position[0]);
+ min_z = min_z.min(position[2]);
+ max_z = max_z.max(position[2]);
+ }
+ let extent = (max_x - min_x).max(max_z - min_z);
+ if !extent.is_finite() || extent <= f32::EPSILON {
+ return Err(VulkanAssetMeshError::DegenerateViewExtent);
+ }
+ let center_x = (min_x + max_x) * 0.5;
+ let center_z = (min_z + max_z) * 0.5;
+ let scale = 1.6 / extent;
+ let vertices = model
+ .positions
+ .iter()
+ .map(|position| VulkanStaticVertex {
+ position: [
+ (position[0] - center_x) * scale,
+ (position[2] - center_z) * scale,
+ ],
+ color: [0.82, 0.72, 0.31],
+ })
+ .collect();
+
+ Ok(VulkanStaticMesh { vertices, indices })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use fparkan_msh::{Batch, ModelAsset};
+
+ fn model(positions: Vec<[f32; 3]>, indices: Vec<u16>, batches: Vec<Batch>) -> ModelAsset {
+ ModelAsset {
+ node_stride: 0,
+ node_count: 0,
+ nodes_raw: Vec::new(),
+ slots: Vec::new(),
+ positions,
+ normals: None,
+ uv0: None,
+ indices,
+ batches,
+ node_names: None,
+ }
+ }
+
+ fn batch(index_start: u32, index_count: u16, base_vertex: u32) -> Batch {
+ Batch {
+ batch_flags: 0,
+ material_index: 0,
+ opaque4: 0,
+ opaque6: 0,
+ index_count,
+ index_start,
+ opaque14: 0,
+ base_vertex,
+ }
+ }
+
+ #[test]
+ fn projects_xz_geometry_and_applies_base_vertex() {
+ let mesh = project_msh_to_static_mesh(&model(
+ vec![
+ [99.0, 0.0, 99.0],
+ [-2.0, 4.0, -1.0],
+ [2.0, 8.0, -1.0],
+ [-2.0, 1.0, 3.0],
+ ],
+ vec![0, 1, 2],
+ vec![batch(0, 3, 1)],
+ ))
+ .expect("representable MSH");
+
+ assert_eq!(mesh.indices, vec![1, 2, 3]);
+ assert_eq!(mesh.vertices[1].position, [-0.8, -0.8]);
+ assert_eq!(mesh.vertices[2].position, [0.8, -0.8]);
+ assert_eq!(mesh.vertices[3].position, [-0.8, 0.8]);
+ }
+}
diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs
index 0b6f1f2..9433759 100644
--- a/adapters/fparkan-render-vulkan/src/ffi.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi.rs
@@ -26,6 +26,8 @@
//!
//! This crate is the declared low-level Vulkan boundary.
+#[path = "asset_mesh.rs"]
+mod asset_mesh;
mod capabilities;
mod instance;
mod resources;
@@ -37,6 +39,7 @@ mod swapchain;
mod swapchain_resources;
mod validation;
+pub use self::asset_mesh::{project_msh_to_static_mesh, VulkanAssetMeshError};
pub use self::capabilities::{
probe_vulkan_runtime_capabilities, probe_vulkan_runtime_capabilities_for_request,
VulkanRuntimeCapabilityError, VulkanRuntimeCapabilityProbe,
diff --git a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs
index 41dfc0f..dd68ef4 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs
@@ -337,7 +337,9 @@ fn create_graphics_pipeline(
.rasterizer_discard_enable(false)
.polygon_mode(vk::PolygonMode::FILL)
.line_width(1.0)
- .cull_mode(vk::CullModeFlags::BACK)
+ // Static MSH viewer mode has no original material/cull state yet; draw
+ // both windings so every source batch is visible during the bridge.
+ .cull_mode(vk::CullModeFlags::NONE)
.front_face(vk::FrontFace::CLOCKWISE)
.depth_bias_enable(false);
let multisample_state = vk::PipelineMultisampleStateCreateInfo::default()