From bfb048698ecbec09e745f5733b8f5df45225d268 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 18 Jul 2026 14:53:11 +0400 Subject: [PATCH] feat(render): bridge legacy camera to Vulkan --- .../shaders/manifest.json | 2 +- .../shaders/triangle.frag | 1 + .../shaders/triangle.frag.spv | Bin 1184 -> 1184 bytes .../shaders/triangle.vert | 8 +- .../shaders/triangle.vert.spv | Bin 1160 -> 1432 bytes .../fparkan-render-vulkan/src/asset_mesh.rs | 22 +++- adapters/fparkan-render-vulkan/src/ffi.rs | 6 +- .../src/ffi/resources.rs | 2 +- .../fparkan-render-vulkan/src/ffi/smoke.rs | 24 +++- .../src/ffi/smoke_types.rs | 120 ++++++++++++++++-- .../src/ffi/swapchain_resources.rs | 22 ++-- .../fparkan-render-vulkan/src/ffi/tests.rs | 10 +- .../src/shader_manifest.rs | 8 +- apps/fparkan-game/src/main.rs | 5 +- apps/fparkan-vulkan-smoke/src/main.rs | 4 +- docs/tomes/05-render.md | 18 ++- 16 files changed, 204 insertions(+), 48 deletions(-) diff --git a/adapters/fparkan-render-vulkan/shaders/manifest.json b/adapters/fparkan-render-vulkan/shaders/manifest.json index d187cdb..89df3ce 100644 --- a/adapters/fparkan-render-vulkan/shaders/manifest.json +++ b/adapters/fparkan-render-vulkan/shaders/manifest.json @@ -1 +1 @@ -{"schema":2,"target_env":"vulkan1.1","compiler":{"name":"glslangValidator","version":"11:16.3.0","binary_sha256":"9bcd69d830b350aaa6e2254915ff74e46070e217b67f38daad27c1fc1f22910f"},"validator":{"name":"spirv-val","version":"SPIRV-Tools v2026.2 unknown hash, 2026-04-29T17:02:58+00:00","binary_sha256":"f6d5b96ff19f073f3af0c0bcfa0c18702d288d3ec598efc242d01cd104d8354f"},"modules":[{"name":"triangle.vert","stage":"vertex","entry_point":"main","source_path":"adapters/fparkan-render-vulkan/shaders/triangle.vert","source_sha256":"82f9ee39a30b094203041205954576ca820d75771421bb27ddd300359e4e9043","spirv_path":"adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","word_count":290,"sha256":"1984662f78873b70135ec444ccd86d86fa66dfca3f615d19ddaf0cda1587d4cf","descriptor_sets":0,"push_constant_bytes":0,"compile_command":"glslangValidator -V --target-env vulkan1.1 -S vert -e main adapters/fparkan-render-vulkan/shaders/triangle.vert -o adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","validate_command":"spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","interface_hash":"23e1d3d9d32e7f7ec0b9ca87f8b86be8f8363c7eb5d745fc5a157cb8433eb138"},{"name":"triangle.frag","stage":"fragment","entry_point":"main","source_path":"adapters/fparkan-render-vulkan/shaders/triangle.frag","source_sha256":"f9162fccce56111e85fdf0f58251798393c37aa23f9f3d979ba404bf6773c27b","spirv_path":"adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","word_count":296,"sha256":"536a5c9a4389f9d34ca11a25f20d0acbc53d3eb0782c18375326647319336a85","descriptor_sets":1,"push_constant_bytes":4,"compile_command":"glslangValidator -V --target-env vulkan1.1 -S frag -e main adapters/fparkan-render-vulkan/shaders/triangle.frag -o adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","validate_command":"spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","interface_hash":"7692547c3195074fefc2321e1f4d4a09a3d83c65465cefd2237c8bae1df58d88"}],"manifest_hash":"5a16fb791e86bb790cd2d85151627e0d39193c5c073a9294130f149d4ff5ba58"} +{"schema":2,"target_env":"vulkan1.1","compiler":{"name":"glslangValidator","version":"11:16.3.0","binary_sha256":"9bcd69d830b350aaa6e2254915ff74e46070e217b67f38daad27c1fc1f22910f"},"validator":{"name":"spirv-val","version":"SPIRV-Tools v2026.2 unknown hash, 2026-04-29T17:02:58+00:00","binary_sha256":"f6d5b96ff19f073f3af0c0bcfa0c18702d288d3ec598efc242d01cd104d8354f"},"modules":[{"name":"triangle.vert","stage":"vertex","entry_point":"main","source_path":"adapters/fparkan-render-vulkan/shaders/triangle.vert","source_sha256":"fe3721202477220d2d9677b6572d7f3baecb374e94f04eea9d7286ed5e98b6c9","spirv_path":"adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","word_count":358,"sha256":"4e2051ac43b933a57e5557e596bcc95952b4efbfcb45315180f89581e094398d","descriptor_sets":0,"push_constant_bytes":64,"compile_command":"glslangValidator -V --target-env vulkan1.1 -S vert -e main adapters/fparkan-render-vulkan/shaders/triangle.vert -o adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","validate_command":"spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","interface_hash":"f76cc92b9aaa2261eda33bd320503e1be100a2dd7e1a9fcfe13351678dda2a06"},{"name":"triangle.frag","stage":"fragment","entry_point":"main","source_path":"adapters/fparkan-render-vulkan/shaders/triangle.frag","source_sha256":"fb3ed435af48e4fb4a817f160b467f7a561c7c547c87b6f0c1e89f5f58af7329","spirv_path":"adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","word_count":296,"sha256":"8ab7bf835e166892b04b10fa1100258daf355d2f693ad311d014dbee22de5c7b","descriptor_sets":1,"push_constant_bytes":68,"compile_command":"glslangValidator -V --target-env vulkan1.1 -S frag -e main adapters/fparkan-render-vulkan/shaders/triangle.frag -o adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","validate_command":"spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","interface_hash":"91552fce09a408a2baf8a608bc642b6632372d3d2316217051384159ba14cee2"}],"manifest_hash":"cf471972978ddb5c8919711ad64c93d0d64e18b1f590a89ce014f1e2a743cfb5"} diff --git a/adapters/fparkan-render-vulkan/shaders/triangle.frag b/adapters/fparkan-render-vulkan/shaders/triangle.frag index 22bdef0..bcc3d43 100644 --- a/adapters/fparkan-render-vulkan/shaders/triangle.frag +++ b/adapters/fparkan-render-vulkan/shaders/triangle.frag @@ -7,6 +7,7 @@ layout(location = 0) out vec4 out_color; layout(set = 0, binding = 0) uniform sampler2D base_color; layout(push_constant) uniform AlphaTestConstants { + layout(offset = 64) float alpha_cutoff; } alpha_test; diff --git a/adapters/fparkan-render-vulkan/shaders/triangle.frag.spv b/adapters/fparkan-render-vulkan/shaders/triangle.frag.spv index 9fa123b6cf1384f6ed240bf6fbcbf15cf748f9a6..46b1e6f5d6b2262cfa9a5c948d1dc9be9fc955a0 100644 GIT binary patch delta 14 VcmZ3$xqx#+4`JVvT%tJfg+#f(PU)|Fno) zCuU#tvF3Emw8rZurB$Lt)bVRCEu>OsEEBT5v|MXRQ$DnJ)hoZus4<3Uko%1L1n_#J z`UY<a2>G|!Bku#NG5l$VR?`lPx+AOU sx82i8^N$Jk-$4y3(4skwl{_IdTt!+?i=PtfgciK!GsIQ*&F5YaKTw}3W&i*H delta 226 zcmbQi-NDJr%%sfDz|6?N&A`E+I*~V;QD$Q1Pi9sInaL{|*D|v($ZVd=l+0*q&A`GS zz`(#D1H_C#tPSL|0cklP1_2mf9?Wa7R{+W@PcCGMudxNPc!8p_Kn&6y$iNELsQ?uN zQgAUvs2IqG00wpjVIZvp#0o$Rk^|WWvcVB(hBA;=0b)rYmIh)w24*lHr2a3^jTS%* E05E3~5&!@I diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs index 69de34f..fa7c28e 100644 --- a/adapters/fparkan-render-vulkan/src/asset_mesh.rs +++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs @@ -142,7 +142,11 @@ pub fn project_msh_to_static_mesh_in_xy_frame( position[2] * scale[2] + translation[2], ]; Ok(VulkanStaticVertex { - position: frame.project(transformed), + position: [ + frame.project(transformed)[0], + frame.project(transformed)[1], + 0.0, + ], color: [0.82, 0.72, 0.31], // Iron3D stores Res5 UV0 as signed fixed point with 1/1024 units. // Models that omit this optional stream retain the static viewer's @@ -232,7 +236,11 @@ pub fn project_land_msh_to_static_mesh_in_xy_frame( return Err(VulkanAssetMeshError::NonFinitePosition); } Ok(VulkanStaticVertex { - position: frame.project(*position), + position: [ + frame.project(*position)[0], + frame.project(*position)[1], + 0.0, + ], color: [0.31, 0.58, 0.27], uv: terrain .uv0 @@ -380,9 +388,9 @@ mod tests { alpha_test_reference: 0, }] ); - 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]); + assert_eq!(mesh.vertices[1].position, [-0.8, -0.8, 0.0]); + assert_eq!(mesh.vertices[2].position, [0.8, -0.8, 0.0]); + assert_eq!(mesh.vertices[3].position, [-0.8, 0.8, 0.0]); } #[test] @@ -466,8 +474,8 @@ mod tests { assert_eq!(mesh.indices, vec![0, 1, 2, 1, 3, 2]); assert_eq!(mesh.draw_ranges.len(), 1); assert_eq!(mesh.draw_ranges[0].index_count, 6); - assert_eq!(mesh.vertices[0].position, [-0.8, -0.8]); - assert_eq!(mesh.vertices[3].position, [0.8, 0.8]); + assert_eq!(mesh.vertices[0].position, [-0.8, -0.8, 0.0]); + assert_eq!(mesh.vertices[3].position, [0.8, 0.8, 0.0]); assert_eq!(mesh.vertices[0].uv, [1.0, -0.5]); assert_eq!(mesh.vertices[2].uv, [-1.0, 0.5]); } diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs index 260ff03..056afcc 100644 --- a/adapters/fparkan-render-vulkan/src/ffi.rs +++ b/adapters/fparkan-render-vulkan/src/ffi.rs @@ -71,8 +71,8 @@ pub use self::smoke_types::{ VulkanReadbackArtifact, VulkanSmokeBootstrapProgress, VulkanSmokeBootstrapSnapshot, VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError, VulkanSmokeRendererReport, VulkanSmokeShutdownReport, - VulkanStaticDrawRange, VulkanStaticMaterial, VulkanStaticMesh, VulkanStaticTexture, - VulkanStaticVertex, VulkanValidationReport, + VulkanStaticCamera, VulkanStaticDrawRange, VulkanStaticMaterial, VulkanStaticMesh, + VulkanStaticTexture, VulkanStaticVertex, VulkanValidationReport, }; #[cfg(test)] use self::surface::extension_name; @@ -497,7 +497,7 @@ const fn spirv_words(bytes: &[u8]) -> [u32; WORD_COUNT] words } -static TRIANGLE_VERTEX_SHADER_DATA: [u32; 290] = +static TRIANGLE_VERTEX_SHADER_DATA: [u32; 358] = spirv_words(include_bytes!("../shaders/triangle.vert.spv")); static TRIANGLE_FRAGMENT_SHADER_DATA: [u32; 296] = spirv_words(include_bytes!("../shaders/triangle.frag.spv")); 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 { - let mut bytes = Vec::with_capacity(mesh.vertices.len() * 7 * std::mem::size_of::()); + let mut bytes = Vec::with_capacity(mesh.vertices.len() * 8 * std::mem::size_of::()); 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 { + 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], }, @@ -250,6 +305,44 @@ mod static_mesh_tests { assert_eq!(mesh.validate(), Ok(())); } + #[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 { @@ -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, pub(super) draw_ranges: Vec, pub(super) draw_texture_indices: Vec, + pub(super) camera: VulkanStaticCamera, pub(super) frame_sync: Vec, pub(super) images_in_flight: Vec, 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 { 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::()).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::()).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::()).unwrap_or(u32::MAX)) + .stride(u32::try_from(8 * std::mem::size_of::()).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::()).unwrap_or(u32::MAX)), + .offset(u32::try_from(3 * std::mem::size_of::()).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::()).unwrap_or(u32::MAX)), + .offset(u32::try_from(6 * std::mem::size_of::()).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" ); } diff --git a/adapters/fparkan-render-vulkan/src/shader_manifest.rs b/adapters/fparkan-render-vulkan/src/shader_manifest.rs index cb4824d..be86e50 100644 --- a/adapters/fparkan-render-vulkan/src/shader_manifest.rs +++ b/adapters/fparkan-render-vulkan/src/shader_manifest.rs @@ -20,7 +20,7 @@ pub(crate) const SPIRV_VALIDATOR_BINARY_SHA256: &str = pub(crate) const TRIANGLE_VERTEX_SOURCE_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.vert"; pub(crate) const TRIANGLE_VERTEX_SOURCE_SHA256: &str = - "82f9ee39a30b094203041205954576ca820d75771421bb27ddd300359e4e9043"; + "fe3721202477220d2d9677b6572d7f3baecb374e94f04eea9d7286ed5e98b6c9"; pub(crate) const TRIANGLE_VERTEX_SPIRV_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.vert.spv"; pub(crate) const TRIANGLE_VERTEX_COMPILE_COMMAND: &str = @@ -29,7 +29,7 @@ pub(crate) const TRIANGLE_VERTEX_VALIDATE_COMMAND: &str = "spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.vert.spv"; const TRIANGLE_FRAGMENT_SOURCE_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.frag"; const TRIANGLE_FRAGMENT_SOURCE_SHA256: &str = - "f9162fccce56111e85fdf0f58251798393c37aa23f9f3d979ba404bf6773c27b"; + "fb3ed435af48e4fb4a817f160b467f7a561c7c547c87b6f0c1e89f5f58af7329"; const TRIANGLE_FRAGMENT_SPIRV_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.frag.spv"; const TRIANGLE_FRAGMENT_COMPILE_COMMAND: &str = @@ -213,7 +213,7 @@ pub fn triangle_shader_manifest() -> Vec { stage: VulkanShaderStage::Vertex, entry_point: "main", descriptor_sets: 0, - push_constant_bytes: 0, + push_constant_bytes: 64, source_path: TRIANGLE_VERTEX_SOURCE_PATH, source_sha256: TRIANGLE_VERTEX_SOURCE_SHA256, spirv_path: TRIANGLE_VERTEX_SPIRV_PATH, @@ -226,7 +226,7 @@ pub fn triangle_shader_manifest() -> Vec { stage: VulkanShaderStage::Fragment, entry_point: "main", descriptor_sets: 1, - push_constant_bytes: 4, + push_constant_bytes: 68, source_path: TRIANGLE_FRAGMENT_SOURCE_PATH, source_sha256: TRIANGLE_FRAGMENT_SOURCE_SHA256, spirv_path: TRIANGLE_FRAGMENT_SPIRV_PATH, diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs index d2561b6..88bd533 100644 --- a/apps/fparkan-game/src/main.rs +++ b/apps/fparkan-game/src/main.rs @@ -32,8 +32,8 @@ use fparkan_render::{ use fparkan_render_vulkan::{ project_land_msh_to_static_mesh_in_xy_frame, project_msh_to_static_mesh_in_xy_frame, VulkanPlanningBackend, VulkanSmokeFrameOutcome, VulkanSmokeRenderer, - VulkanSmokeRendererCreateInfo, VulkanStaticMaterial, VulkanStaticMesh, VulkanStaticTexture, - VulkanStaticXyFrame, + VulkanSmokeRendererCreateInfo, VulkanStaticCamera, VulkanStaticMaterial, VulkanStaticMesh, + VulkanStaticTexture, VulkanStaticXyFrame, }; use fparkan_runtime::{ create, frame, load_mission, load_mission_static_preview, load_mission_static_preview_roots, @@ -658,6 +658,7 @@ impl ApplicationHandler for StaticVulkanApp { render_request: WinitWindow::default_render_request(), enable_validation: true, mesh: self.mesh.clone(), + camera: VulkanStaticCamera::default(), materials: self.materials.clone(), bootstrap_progress: None, }) { diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs index d4782ff..dea15b4 100644 --- a/apps/fparkan-vulkan-smoke/src/main.rs +++ b/apps/fparkan-vulkan-smoke/src/main.rs @@ -17,7 +17,8 @@ use fparkan_render_vulkan::{ project_land_msh_to_static_mesh, project_msh_to_static_mesh, VulkanReadbackArtifact, VulkanSmokeBootstrapProgress, VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererReport, VulkanSmokeShutdownReport, - VulkanStaticMaterial, VulkanStaticMesh, VulkanStaticTexture, VulkanValidationReport, + VulkanStaticCamera, VulkanStaticMaterial, VulkanStaticMesh, VulkanStaticTexture, + VulkanValidationReport, }; use serde::Serialize; use std::path::PathBuf; @@ -1175,6 +1176,7 @@ impl ApplicationHandler for SmokeApp { render_request: RenderRequest::conservative(), enable_validation: true, mesh: self.mesh.clone(), + camera: VulkanStaticCamera::default(), materials: self .materials .iter() diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index 4f86fef..47d0fc8 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -1412,10 +1412,20 @@ An elevated read-only AutoDemo probe sampled a 1024×768 renderer with near=`0.5`, far=`700`, FOV=`1.3` radians and showed its view-source pointer byte-identical to the active Terrain outer camera's selector-0 block. This proves ownership and the D3D7 boundary, while keeping the earlier -`CBufferingCamera` FOV=`1.04` as a distinct upstream interface value. The -backend-neutral render crate now exposes the two recovered D3D7 matrices, but -does not yet use them as Vulkan matrices: vector and clip-space conversion is -still an explicit next task. +`CBufferingCamera` FOV=`1.04` as a distinct upstream interface value. + +The Vulkan static path now accepts this recovered camera as +`VulkanStaticCamera`. It composes the row-major D3D7 result as +`view * projection`, uploads its 64 bytes through a vertex-stage push constant, +and changes static vertices from XY to XYZ. GLSL reads the same bytes as its +default column-major `mat4`; therefore `matrix * vec4(position, 1)` is the +transpose-equivalent of the original D3D7 row-vector multiplication. Both APIs +use the 0..1 depth range, and this renderer uses a positive viewport height, so +this narrow bridge needs no extra depth or Y correction. The fragment alpha +cutoff moved to byte offset 64 in its own push-constant range. The default +identity camera deliberately preserves the old XY diagnostic preview; feeding +live terrain camera/projection values into mission rendering remains later +runtime wiring, not a claim of scene parity. A fresh no-input launch of the canonical `iron_3d.exe` did create a responsive window titled `Parkan. Железная Стратегия`. A read-only probe then requested