diff options
| author | Valentin Popov <valentin@popov.link> | 2026-06-23 22:13:52 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-06-23 22:13:52 +0300 |
| commit | 5d9e1cbe3877036bea74f69ec037bb7d5d6e9ad5 (patch) | |
| tree | 76ea3cb574f4e047a3cabaea689a4f24c58899c6 | |
| parent | 0e76c2ed7c870cd0312b4762de344a0830324489 (diff) | |
| download | fparkan-5d9e1cbe3877036bea74f69ec037bb7d5d6e9ad5.tar.xz fparkan-5d9e1cbe3877036bea74f69ec037bb7d5d6e9ad5.zip | |
feat: add Vulkan frame submission plan
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/lib.rs | 156 | ||||
| -rw-r--r-- | fixtures/acceptance/coverage.tsv | 2 | ||||
| -rw-r--r-- | fixtures/acceptance/stage_0_2_roadmap.md | 2 |
3 files changed, 159 insertions, 1 deletions
diff --git a/adapters/fparkan-render-vulkan/src/lib.rs b/adapters/fparkan-render-vulkan/src/lib.rs index 15bbbda..3f63c5f 100644 --- a/adapters/fparkan-render-vulkan/src/lib.rs +++ b/adapters/fparkan-render-vulkan/src/lib.rs @@ -31,7 +31,8 @@ use ash::{khr::surface, vk}; use fparkan_binary::{sha256, sha256_hex}; use fparkan_platform::{NativeWindowHandles, RenderRequest}; use fparkan_render::{ - canonical_capture, FrameOutput, RenderBackend, RenderCommandList, RenderError, + canonical_capture, validate_command_list, FrameOutput, RenderBackend, RenderCommand, + RenderCommandList, RenderError, }; use std::ffi::{CStr, CString}; use std::os::raw::c_char; @@ -906,6 +907,25 @@ pub struct VulkanSwapchainRecreationReport { pub next_extent: (u32, u32), } +/// Deterministic frame submission plan for command buffers and sync objects. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VulkanFrameSubmissionPlan { + /// Report schema version. + pub schema: u32, + /// Frames allowed in flight. + pub frames_in_flight: u32, + /// Swapchain-backed primary command buffers. + pub command_buffers: u32, + /// Binary semaphores allocated per frame. + pub semaphores_per_frame: u32, + /// Fences allocated per frame. + pub fences_per_frame: u32, + /// Draw commands encoded into the frame. + pub draw_count: u32, + /// Total indexed vertices submitted by draw commands. + pub indexed_vertex_count: u32, +} + /// Synthetic physical-device capabilities used by negative tests and reports. #[derive(Clone, Debug, Eq, PartialEq)] pub struct VulkanPhysicalDeviceRecord { @@ -1137,6 +1157,39 @@ pub const fn swapchain_recreation_report( } } +/// Builds a deterministic frame submission plan for a validated command list. +/// +/// Stage 0 keeps this as a pure planning boundary so command-pool, command-buffer +/// and synchronization policy can be tested without requiring a native surface. +/// +/// # Errors +/// +/// Returns [`RenderError`] when the command list has invalid frame framing, +/// ordering, draw ranges, mesh bounds, or non-finite transforms. +pub fn plan_vulkan_frame_submission( + swapchain: &VulkanSwapchainPlan, + commands: &RenderCommandList, +) -> Result<VulkanFrameSubmissionPlan, RenderError> { + validate_command_list(commands)?; + let mut draw_count = 0_u32; + let mut indexed_vertex_count = 0_u32; + for command in &commands.commands { + if let RenderCommand::Draw(draw) = command { + draw_count = draw_count.saturating_add(1); + indexed_vertex_count = indexed_vertex_count.saturating_add(draw.range.count); + } + } + Ok(VulkanFrameSubmissionPlan { + schema: 1, + frames_in_flight: swapchain.image_count.clamp(1, 2), + command_buffers: swapchain.image_count, + semaphores_per_frame: 2, + fences_per_frame: 1, + draw_count, + indexed_vertex_count, + }) +} + fn validate_device( device: &VulkanPhysicalDeviceRecord, ) -> Result<VulkanCapabilityReport, VulkanCapabilityError> { @@ -1300,6 +1353,28 @@ pub fn render_swapchain_recreation_report_json(report: &VulkanSwapchainRecreatio out } +/// Renders a deterministic JSON frame submission plan. +#[must_use] +pub fn render_frame_submission_plan_json(plan: &VulkanFrameSubmissionPlan) -> String { + let mut out = String::new(); + out.push_str("{\"schema\":"); + out.push_str(&plan.schema.to_string()); + out.push_str(",\"frames_in_flight\":"); + out.push_str(&plan.frames_in_flight.to_string()); + out.push_str(",\"command_buffers\":"); + out.push_str(&plan.command_buffers.to_string()); + out.push_str(",\"semaphores_per_frame\":"); + out.push_str(&plan.semaphores_per_frame.to_string()); + out.push_str(",\"fences_per_frame\":"); + out.push_str(&plan.fences_per_frame.to_string()); + out.push_str(",\"draw_count\":"); + out.push_str(&plan.draw_count.to_string()); + out.push_str(",\"indexed_vertex_count\":"); + out.push_str(&plan.indexed_vertex_count.to_string()); + out.push('}'); + out +} + fn format_api_version(version: u32) -> String { format!( "{}.{}.{}", @@ -1345,6 +1420,8 @@ pub struct VulkanBackendReport { pub resize_rebuilds: u64, /// Last render request observed. pub request: RenderRequest, + /// Last deterministic frame submission plan. + pub last_frame_submission: Option<VulkanFrameSubmissionPlan>, } impl Default for VulkanBackendReport { @@ -1359,6 +1436,7 @@ impl Default for VulkanBackendReport { presents: 0, resize_rebuilds: 0, request: RenderRequest::conservative(), + last_frame_submission: None, } } } @@ -1368,6 +1446,7 @@ impl Default for VulkanBackendReport { pub struct VulkanBackend { state: VulkanBackendState, report: VulkanBackendReport, + swapchain_plan: VulkanSwapchainPlan, } impl Default for VulkanBackend { @@ -1383,6 +1462,7 @@ impl VulkanBackend { Self { state: VulkanBackendState::Ready, report: VulkanBackendReport::default(), + swapchain_plan: default_stage0_swapchain_plan(), } } @@ -1398,6 +1478,17 @@ impl VulkanBackend { self.report.request } + /// Replaces active swapchain plan used for frame submission planning. + pub fn set_swapchain_plan(&mut self, plan: VulkanSwapchainPlan) { + self.swapchain_plan = plan; + } + + /// Returns active swapchain plan. + #[must_use] + pub const fn swapchain_plan(&self) -> &VulkanSwapchainPlan { + &self.swapchain_plan + } + /// Returns adapter state. #[must_use] pub const fn state(&self) -> VulkanBackendState { @@ -1424,14 +1515,29 @@ impl RenderBackend for VulkanBackend { return Err(RenderError::InvalidRange); } let capture = canonical_capture(commands)?; + let frame_plan = plan_vulkan_frame_submission(&self.swapchain_plan, commands)?; self.report.frames_executed = self.report.frames_executed.saturating_add(1); self.report.submissions = self.report.submissions.saturating_add(1); self.report.last_capture_size = capture.len(); + self.report.last_frame_submission = Some(frame_plan); self.simulate_present(); Ok(FrameOutput) } } +fn default_stage0_swapchain_plan() -> VulkanSwapchainPlan { + VulkanSwapchainPlan { + schema: 1, + extent: (1, 1), + format: VulkanSurfaceFormat { + format: vk::Format::B8G8R8A8_SRGB.as_raw(), + color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(), + }, + present_mode: vk::PresentModeKHR::FIFO.as_raw(), + image_count: 2, + } +} + #[cfg(test)] mod tests { use super::*; @@ -1470,6 +1576,54 @@ mod tests { assert_eq!(backend.report().submissions, 1); assert_eq!(backend.report().presents, 1); assert!(backend.report().last_capture_size > 0); + assert_eq!( + backend.report().last_frame_submission, + Some(VulkanFrameSubmissionPlan { + schema: 1, + frames_in_flight: 2, + command_buffers: 2, + semaphores_per_frame: 2, + fences_per_frame: 1, + draw_count: 1, + indexed_vertex_count: 3, + }) + ); + Ok(()) + } + + #[test] + fn frame_submission_plan_json_is_stable() -> Result<(), RenderError> { + let commands = fparkan_render::RenderCommandList { + commands: vec![ + RenderCommand::BeginFrame, + RenderCommand::Draw(DrawCommand { + id: DrawId(11), + phase: RenderPhase::Opaque, + object_id: None, + mesh: GpuMeshId(1), + material: GpuMaterialId(2), + transform: [1.0; 16], + range: IndexRange { start: 0, count: 3 }, + stable_order: 7, + }), + RenderCommand::EndFrame, + ], + }; + let swapchain = VulkanSwapchainPlan { + image_count: 3, + ..default_stage0_swapchain_plan() + }; + + let plan = plan_vulkan_frame_submission(&swapchain, &commands)?; + + assert_eq!(plan.frames_in_flight, 2); + assert_eq!(plan.command_buffers, 3); + assert_eq!(plan.draw_count, 1); + assert_eq!(plan.indexed_vertex_count, 3); + assert_eq!( + render_frame_submission_plan_json(&plan), + "{\"schema\":1,\"frames_in_flight\":2,\"command_buffers\":3,\"semaphores_per_frame\":2,\"fences_per_frame\":1,\"draw_count\":1,\"indexed_vertex_count\":3}" + ); Ok(()) } diff --git a/fixtures/acceptance/coverage.tsv b/fixtures/acceptance/coverage.tsv index 141520b..ec8e7d7 100644 --- a/fixtures/acceptance/coverage.tsv +++ b/fixtures/acceptance/coverage.tsv @@ -44,6 +44,8 @@ S0-VK-017 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_j S0-VK-018 covered cargo test -p fparkan-render-vulkan --offline triangle_shader_manifest_hashes_are_stable S0-VK-019 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_report_json_is_stable S0-VK-020 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_rejects_invalid_spirv_containers +S0-VK-021 covered cargo test -p fparkan-render-vulkan --offline frame_submission_plan_json_is_stable +S0-VK-022 covered cargo test -p fparkan-render-vulkan --offline backend_tracks_render_request_and_presents S0-LIMIT-001 covered cargo test -p fparkan-binary --offline rejects_count_stride_overflow S0-LIMIT-002 covered cargo test -p fparkan-binary --offline rejects_oversized_declared_allocation_before_read L1-P1-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates diff --git a/fixtures/acceptance/stage_0_2_roadmap.md b/fixtures/acceptance/stage_0_2_roadmap.md index c365485..84aa8a5 100644 --- a/fixtures/acceptance/stage_0_2_roadmap.md +++ b/fixtures/acceptance/stage_0_2_roadmap.md @@ -44,6 +44,8 @@ `S0-VK-018` `S0-VK-019` `S0-VK-020` +`S0-VK-021` +`S0-VK-022` `S0-LIMIT-001` `S0-LIMIT-002` `L1-P1-NRES-001` |
