diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index 5db7e3b..63066b4 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -314,6 +314,32 @@ impl VulkanSmokeRenderer { self.swapchain_recreate_count } + /// Returns the camera that will be uploaded for the next recorded frame. + #[must_use] + pub const fn camera(&self) -> super::VulkanStaticCamera { + self.camera + } + + /// Replaces the camera for subsequent frames without recreating GPU resources. + /// + /// # Errors + /// + /// Returns [`VulkanSmokeRendererError::InvalidStaticCamera`] when a matrix + /// element is non-finite. The caller must update only between + /// [`Self::draw_frame`] calls on the renderer-owning thread. + pub fn set_camera( + &mut self, + camera: super::VulkanStaticCamera, + ) -> Result<(), VulkanSmokeRendererError> { + if !camera.is_finite() { + return Err(VulkanSmokeRendererError::InvalidStaticCamera { + context: "non-finite clip_from_world matrix", + }); + } + self.camera = camera; + Ok(()) + } + /// Explicitly idles and tears down the renderer while the native window is still alive. /// /// # Errors diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index 14301f2..47cab7d 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -72,9 +72,23 @@ impl VulkanStaticCamera { ) -> 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), - }) + Self::from_row_major_view_projection(view, projection) + } + + /// Builds a camera from finite row-major view and projection matrices. + /// + /// This is the runtime-facing form of the contract. It is intentionally + /// independent of the D3D7 recovery path so a future native camera + /// controller can submit its per-frame matrices without recreating Vulkan + /// resources. + #[must_use] + pub fn from_row_major_view_projection(view: [f32; 16], projection: [f32; 16]) -> Option { + view.iter() + .chain(projection.iter()) + .all(|value| value.is_finite()) + .then_some(Self { + clip_from_world: multiply_row_major(view, projection), + }) } /// Returns whether every matrix element is finite. @@ -343,6 +357,22 @@ mod static_mesh_tests { assert_eq!(camera.clip_from_world[7], 0.65_f32.sin()); } + #[test] + fn static_camera_accepts_finite_runtime_matrices_and_rejects_nan() { + let identity = VulkanStaticCamera::default().clip_from_world; + + assert_eq!( + VulkanStaticCamera::from_row_major_view_projection(identity, identity), + Some(VulkanStaticCamera::default()) + ); + let mut invalid = identity; + invalid[5] = f32::NAN; + assert_eq!( + VulkanStaticCamera::from_row_major_view_projection(invalid, identity), + None + ); + } + #[test] fn static_mesh_rejects_bad_triangle_topology_and_indices() { let no_vertices = VulkanStaticMesh { diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs index dea15b4..ec2d19c 100644 --- a/apps/fparkan-vulkan-smoke/src/main.rs +++ b/apps/fparkan-vulkan-smoke/src/main.rs @@ -1242,6 +1242,11 @@ impl ApplicationHandler for SmokeApp { event_loop.exit(); return; }; + if let Err(error) = renderer.set_camera(VulkanStaticCamera::default()) { + self.error = Some(error.to_string()); + event_loop.exit(); + return; + } match renderer.draw_frame() { Ok(VulkanSmokeFrameOutcome::Presented) => { self.frames_presented = self.frames_presented.saturating_add(1); diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index 19f1f92..a3165c3 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -1437,6 +1437,15 @@ on its XY diagnostic projection until a runtime supplies a real legacy camera; selecting source-world coordinates with identity camera would be a misleading empty/off-screen viewer rather than a compatibility result. +`VulkanSmokeRenderer::set_camera` now accepts a new finite camera between frame +submissions and uses it for the next command buffer without rebuilding the +swapchain, buffers, descriptors or pipelines. `VulkanStaticCamera` also accepts +generic finite row-major view/projection pairs, so the final game camera +controller need not be coupled to the D3D7 recovery type. The native Windows +smoke executes that update before each of 300 validation-clean frames. This is +the renderer-side cadence contract only; it does not yet supply a gameplay +camera controller or claim camera movement 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 `PROCESS_QUERY_INFORMATION | PROCESS_VM_READ` and attempted to read the known