aboutsummaryrefslogtreecommitdiff
path: root/adapters/fparkan-render-vulkan/src/ffi
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-07-18 13:53:11 +0300
committerValentin Popov <valentin@popov.link>2026-07-18 13:53:11 +0300
commitbfb048698ecbec09e745f5733b8f5df45225d268 (patch)
treee0f2b041f38ff6be71afec4459bb35e7ed7db83e /adapters/fparkan-render-vulkan/src/ffi
parent94de743581db6f2bb68285951c8c2a0088bc9ce6 (diff)
downloadfparkan-bfb048698ecbec09e745f5733b8f5df45225d268.tar.xz
fparkan-bfb048698ecbec09e745f5733b8f5df45225d268.zip
feat(render): bridge legacy camera to Vulkan
Diffstat (limited to 'adapters/fparkan-render-vulkan/src/ffi')
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/resources.rs2
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke.rs24
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs120
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs22
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/tests.rs10
5 files changed, 154 insertions, 24 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi/resources.rs b/adapters/fparkan-render-vulkan/src/ffi/resources.rs
index 6f2005e..b7e84bd 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/resources.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/resources.rs
@@ -211,7 +211,7 @@ pub(super) fn create_static_mesh_vertex_buffer(
device: &VulkanLogicalDeviceProbe,
mesh: &VulkanStaticMesh,
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
- let mut bytes = Vec::with_capacity(mesh.vertices.len() * 7 * std::mem::size_of::<f32>());
+ let mut bytes = Vec::with_capacity(mesh.vertices.len() * 8 * std::mem::size_of::<f32>());
for vertex in &mesh.vertices {
for value in vertex
.position
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
index 11c3377..5db7e3b 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
@@ -111,6 +111,11 @@ impl VulkanSmokeRenderer {
.mesh
.validate()
.map_err(|context| VulkanSmokeRendererError::InvalidStaticMesh { context })?;
+ if !create_info.camera.is_finite() {
+ return Err(VulkanSmokeRendererError::InvalidStaticCamera {
+ context: "non-finite clip_from_world matrix",
+ });
+ }
let draw_texture_indices =
resolve_draw_texture_indices(&create_info.mesh.draw_ranges, &create_info.materials)
.map_err(|context| VulkanSmokeRendererError::InvalidStaticMesh { context })?;
@@ -226,6 +231,7 @@ impl VulkanSmokeRenderer {
textures,
draw_ranges: create_info.mesh.draw_ranges.clone(),
draw_texture_indices,
+ camera: create_info.camera,
frame_sync: Vec::new(),
images_in_flight: Vec::new(),
current_frame: 0,
@@ -671,6 +677,14 @@ impl VulkanSmokeRenderer {
0,
vk::IndexType::UINT16,
);
+ let clip_from_world = matrix_bytes(self.camera.clip_from_world);
+ device.device().cmd_push_constants(
+ command_buffer,
+ resources.pipeline_layout,
+ vk::ShaderStageFlags::VERTEX,
+ 0,
+ &clip_from_world,
+ );
for (range, &texture_index) in self.draw_ranges.iter().zip(&self.draw_texture_indices) {
let pipeline = resources.pipelines.get(&range.pipeline_key()).ok_or(
VulkanSmokeRendererError::InvariantViolation {
@@ -700,7 +714,7 @@ impl VulkanSmokeRenderer {
command_buffer,
resources.pipeline_layout,
vk::ShaderStageFlags::FRAGMENT,
- 0,
+ 64,
&alpha_cutoff,
);
device.device().cmd_draw_indexed(
@@ -928,6 +942,14 @@ impl VulkanSmokeRenderer {
}
}
+fn matrix_bytes(matrix: [f32; 16]) -> [u8; 64] {
+ let mut bytes = [0; 64];
+ for (index, value) in matrix.into_iter().enumerate() {
+ bytes[index * 4..(index + 1) * 4].copy_from_slice(&value.to_ne_bytes());
+ }
+ bytes
+}
+
fn fnv1a64(bytes: &[u8]) -> u64 {
bytes.iter().fold(0xcbf2_9ce4_8422_2325_u64, |hash, byte| {
(hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3)
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
index b94e460..14301f2 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
@@ -1,6 +1,6 @@
use ash::vk;
use fparkan_platform::{NativeWindowHandles, RenderRequest};
-use fparkan_render::{LegacyPipelineState, PipelineKey};
+use fparkan_render::{LegacyD3d7Projection, LegacyPipelineState, PipelineKey, RawCameraTransform};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
@@ -26,10 +26,11 @@ pub struct VulkanSmokeRendererCreateInfo {
/// 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,
+ /// Camera contract for static geometry.
+ ///
+ /// The default identity matrix retains the initial XY diagnostic viewer.
+ pub camera: VulkanStaticCamera,
/// Material textures uploaded before the first live frame.
///
/// An empty list retains the compatibility white fallback. A singleton list
@@ -43,14 +44,68 @@ pub struct VulkanSmokeRendererCreateInfo {
/// 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],
+ /// World or clip position transformed by [`VulkanStaticCamera`].
+ pub position: [f32; 3],
/// Linear RGB vertex color.
pub color: [f32; 3],
/// Texture coordinate consumed by the static material bridge.
pub uv: [f32; 2],
}
+/// A static geometry camera represented in the shader's matrix memory order.
+///
+/// `clip_from_world` contains row-major D3D7 data. GLSL's default column-major
+/// `mat4` interpretation deliberately transposes that storage, making
+/// `matrix * vec4(position, 1)` equivalent to D3D7's row-vector multiplication.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct VulkanStaticCamera {
+ /// Row-major clip-from-world transform consumed by the vertex shader.
+ pub clip_from_world: [f32; 16],
+}
+
+impl VulkanStaticCamera {
+ /// Builds a static camera from the recovered Ngi32 D3D7 camera contract.
+ #[must_use]
+ pub fn from_legacy_d3d7(
+ transform: RawCameraTransform,
+ projection: LegacyD3d7Projection,
+ ) -> Option<Self> {
+ let view = transform.try_direct3d7_view_row_major()?;
+ let projection = projection.try_direct3d7_projection_row_major()?;
+ Some(Self {
+ clip_from_world: multiply_row_major(view, projection),
+ })
+ }
+
+ /// Returns whether every matrix element is finite.
+ #[must_use]
+ pub fn is_finite(self) -> bool {
+ self.clip_from_world.iter().all(|value| value.is_finite())
+ }
+}
+
+impl Default for VulkanStaticCamera {
+ fn default() -> Self {
+ Self {
+ clip_from_world: [
+ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
+ ],
+ }
+ }
+}
+
+fn multiply_row_major(left: [f32; 16], right: [f32; 16]) -> [f32; 16] {
+ let mut result = [0.0; 16];
+ for row in 0..4 {
+ for column in 0..4 {
+ result[row * 4 + column] = (0..4)
+ .map(|inner| left[row * 4 + inner] * right[inner * 4 + column])
+ .sum();
+ }
+ }
+ result
+}
+
/// Static indexed geometry uploaded to live Vulkan buffers.
#[derive(Clone, Debug, PartialEq)]
pub struct VulkanStaticMesh {
@@ -176,17 +231,17 @@ impl VulkanStaticMesh {
Self {
vertices: vec![
VulkanStaticVertex {
- position: [0.0, -0.55],
+ position: [0.0, -0.55, 0.0],
color: [1.0, 0.2, 0.2],
uv: [0.5, 0.0],
},
VulkanStaticVertex {
- position: [0.55, 0.55],
+ position: [0.55, 0.55, 0.0],
color: [0.2, 1.0, 0.2],
uv: [1.0, 1.0],
},
VulkanStaticVertex {
- position: [-0.55, 0.55],
+ position: [-0.55, 0.55, 0.0],
color: [0.2, 0.4, 1.0],
uv: [0.0, 1.0],
},
@@ -251,6 +306,44 @@ mod static_mesh_tests {
}
#[test]
+ fn static_camera_composes_recovered_d3d7_view_before_projection() {
+ let transform = RawCameraTransform {
+ words: [
+ 0.0_f32.to_bits(),
+ (-1.0_f32).to_bits(),
+ 0.0_f32.to_bits(),
+ 10.0_f32.to_bits(),
+ 1.0_f32.to_bits(),
+ 0.0_f32.to_bits(),
+ 0.0_f32.to_bits(),
+ 20.0_f32.to_bits(),
+ 0.0_f32.to_bits(),
+ 0.0_f32.to_bits(),
+ 1.0_f32.to_bits(),
+ 30.0_f32.to_bits(),
+ 0.0_f32.to_bits(),
+ 0.0_f32.to_bits(),
+ 0.0_f32.to_bits(),
+ 1.0_f32.to_bits(),
+ ],
+ };
+ let projection = LegacyD3d7Projection {
+ viewport: [0, 0, 1024, 768],
+ near_plane: 0.5,
+ far_plane: 700.0,
+ field_of_view_radians: 1.3,
+ };
+
+ let camera = VulkanStaticCamera::from_legacy_d3d7(transform, projection)
+ .expect("recovered camera inputs are valid");
+
+ assert!(camera.is_finite());
+ assert_eq!(camera.clip_from_world[0], 0.65_f32.cos());
+ assert_eq!(camera.clip_from_world[6], 700.0_f32 / 699.5);
+ assert_eq!(camera.clip_from_world[7], 0.65_f32.sin());
+ }
+
+ #[test]
fn static_mesh_rejects_bad_triangle_topology_and_indices() {
let no_vertices = VulkanStaticMesh {
vertices: Vec::new(),
@@ -547,6 +640,11 @@ pub enum VulkanSmokeRendererError {
/// Validation failure detail.
context: &'static str,
},
+ /// The static camera matrix contains a non-finite element.
+ InvalidStaticCamera {
+ /// Validation failure detail.
+ context: &'static str,
+ },
/// The submitted static texture cannot be represented by this path.
InvalidStaticTexture {
/// Validation failure detail.
@@ -574,6 +672,9 @@ impl std::fmt::Display for VulkanSmokeRendererError {
write!(f, "{context}: no compatible Vulkan memory type")
}
Self::InvalidStaticMesh { context } => write!(f, "invalid static mesh: {context}"),
+ Self::InvalidStaticCamera { context } => {
+ write!(f, "invalid static camera: {context}")
+ }
Self::InvalidStaticTexture { context } => {
write!(f, "invalid static texture: {context}")
}
@@ -600,6 +701,7 @@ pub struct VulkanSmokeRenderer {
pub(super) textures: Vec<VulkanAllocatedImage>,
pub(super) draw_ranges: Vec<VulkanStaticDrawRange>,
pub(super) draw_texture_indices: Vec<usize>,
+ pub(super) camera: VulkanStaticCamera,
pub(super) frame_sync: Vec<VulkanFrameSync>,
pub(super) images_in_flight: Vec<vk::Fence>,
pub(super) current_frame: usize,
diff --git a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs
index 6d87687..7558db6 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs
@@ -411,10 +411,16 @@ fn create_pipeline_layout(
descriptor_set_layout: vk::DescriptorSetLayout,
) -> Result<vk::PipelineLayout, VulkanSmokeRendererError> {
let set_layouts = [descriptor_set_layout];
- let push_constant_ranges = [vk::PushConstantRange::default()
- .stage_flags(vk::ShaderStageFlags::FRAGMENT)
- .offset(0)
- .size(u32::try_from(std::mem::size_of::<f32>()).unwrap_or(u32::MAX))];
+ let push_constant_ranges = [
+ vk::PushConstantRange::default()
+ .stage_flags(vk::ShaderStageFlags::VERTEX)
+ .offset(0)
+ .size(64),
+ vk::PushConstantRange::default()
+ .stage_flags(vk::ShaderStageFlags::FRAGMENT)
+ .offset(64)
+ .size(u32::try_from(std::mem::size_of::<f32>()).unwrap_or(u32::MAX)),
+ ];
let create_info = vk::PipelineLayoutCreateInfo::default()
.set_layouts(&set_layouts)
.push_constant_ranges(&push_constant_ranges);
@@ -620,24 +626,24 @@ fn create_graphics_pipeline(
];
let vertex_binding = vk::VertexInputBindingDescription::default()
.binding(0)
- .stride(u32::try_from(7 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX))
+ .stride(u32::try_from(8 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX))
.input_rate(vk::VertexInputRate::VERTEX);
let vertex_attributes = [
vk::VertexInputAttributeDescription::default()
.binding(0)
.location(0)
- .format(vk::Format::R32G32_SFLOAT)
+ .format(vk::Format::R32G32B32_SFLOAT)
.offset(0),
vk::VertexInputAttributeDescription::default()
.binding(0)
.location(1)
.format(vk::Format::R32G32B32_SFLOAT)
- .offset(u32::try_from(2 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX)),
+ .offset(u32::try_from(3 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX)),
vk::VertexInputAttributeDescription::default()
.binding(0)
.location(2)
.format(vk::Format::R32G32_SFLOAT)
- .offset(u32::try_from(5 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX)),
+ .offset(u32::try_from(6 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX)),
];
let vertex_bindings = [vertex_binding];
let vertex_input_state = vk::PipelineVertexInputStateCreateInfo::default()
diff --git a/adapters/fparkan-render-vulkan/src/ffi/tests.rs b/adapters/fparkan-render-vulkan/src/ffi/tests.rs
index 0f6f5a8..a842c73 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/tests.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/tests.rs
@@ -669,13 +669,13 @@ fn triangle_shader_manifest_hashes_are_stable() {
TRIANGLE_VERTEX_SOURCE_SHA256
);
assert_eq!(report.modules[0].spirv_path, TRIANGLE_VERTEX_SPIRV_PATH);
- assert_eq!(report.modules[0].word_count, 290);
+ assert_eq!(report.modules[0].word_count, 358);
assert_eq!(
report.modules[0].sha256,
- "1984662f78873b70135ec444ccd86d86fa66dfca3f615d19ddaf0cda1587d4cf"
+ "4e2051ac43b933a57e5557e596bcc95952b4efbfcb45315180f89581e094398d"
);
assert_eq!(report.modules[0].descriptor_sets, 0);
- assert_eq!(report.modules[0].push_constant_bytes, 0);
+ assert_eq!(report.modules[0].push_constant_bytes, 64);
assert_eq!(
report.modules[0].compile_command,
TRIANGLE_VERTEX_COMPILE_COMMAND
@@ -687,11 +687,11 @@ fn triangle_shader_manifest_hashes_are_stable() {
assert!(!report.modules[0].interface_hash.is_empty());
assert_eq!(
report.modules[1].sha256,
- "536a5c9a4389f9d34ca11a25f20d0acbc53d3eb0782c18375326647319336a85"
+ "8ab7bf835e166892b04b10fa1100258daf355d2f693ad311d014dbee22de5c7b"
);
assert_eq!(
report.manifest_hash,
- "5a16fb791e86bb790cd2d85151627e0d39193c5c073a9294130f149d4ff5ba58"
+ "cf471972978ddb5c8919711ad64c93d0d64e18b1f590a89ce014f1e2a743cfb5"
);
}