aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-24 00:53:39 +0300
committerValentin Popov <valentin@popov.link>2026-06-24 00:53:39 +0300
commit021b1c8daca6ac816b3cea3de680e85c3dd6703a (patch)
tree53c717377d21be673bae77d7053974359208f762
parent278567d6de08f1bc3fd7796002214e69216be920 (diff)
downloadfparkan-021b1c8daca6ac816b3cea3de680e85c3dd6703a.tar.xz
fparkan-021b1c8daca6ac816b3cea3de680e85c3dd6703a.zip
feat(vulkan-smoke): run native swapchain acquire/present
-rw-r--r--adapters/fparkan-render-vulkan/src/lib.rs160
-rw-r--r--apps/fparkan-vulkan-smoke/src/main.rs111
2 files changed, 243 insertions, 28 deletions
diff --git a/adapters/fparkan-render-vulkan/src/lib.rs b/adapters/fparkan-render-vulkan/src/lib.rs
index 68009b2..ee89e46 100644
--- a/adapters/fparkan-render-vulkan/src/lib.rs
+++ b/adapters/fparkan-render-vulkan/src/lib.rs
@@ -340,6 +340,28 @@ impl Drop for VulkanLogicalDeviceProbe {
}
}
+impl VulkanLogicalDeviceProbe {
+ /// Returns the graphics queue selected by the Stage 0 policy.
+ #[must_use]
+ pub fn graphics_queue(&self) -> vk::Queue {
+ // SAFETY: The queue-family index belongs to this live logical device.
+ unsafe { self.device.get_device_queue(self.report.graphics_queue_family, 0) }
+ }
+
+ /// Returns the presentation queue selected by the Stage 0 policy.
+ #[must_use]
+ pub fn present_queue(&self) -> vk::Queue {
+ // SAFETY: The queue-family index belongs to this live logical device.
+ unsafe { self.device.get_device_queue(self.report.present_queue_family, 0) }
+ }
+
+ /// Returns a shared reference to the live logical device.
+ #[must_use]
+ pub fn device(&self) -> &ash::Device {
+ &self.device
+ }
+}
+
/// Logical device creation report.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VulkanLogicalDeviceReport {
@@ -370,6 +392,144 @@ impl Drop for VulkanSwapchainProbe {
}
}
+impl VulkanSwapchainProbe {
+ /// Returns the live swapchain handle.
+ #[must_use]
+ pub fn swapchain(&self) -> vk::SwapchainKHR {
+ self.swapchain
+ }
+
+ /// Returns the swapchain extension loader for this live swapchain.
+ #[must_use]
+ pub fn loader(&self) -> &swapchain::Device {
+ &self.loader
+ }
+}
+
+/// Runtime smoke execution result.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanSmokeRunReport {
+ /// Frames successfully advanced through acquire/present.
+ pub frames: u32,
+ /// Number of swapchain recreate attempts.
+ pub swapchain_recreates: u32,
+ /// Number of validation layer errors observed by the smoke path.
+ pub validation_error_count: u32,
+}
+
+/// Errors produced by a native smoke execution path.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum VulkanSmokeRunError {
+ /// Swapchain acquisition failed.
+ AcquireImage {
+ /// Vulkan API result from acquire.
+ result: String,
+ },
+ /// Swapchain present failed.
+ PresentImage {
+ /// Vulkan API result from present.
+ result: String,
+ },
+ /// Swapchain recreation failed.
+ RecreateSwapchain {
+ /// Vulkan API result from resource recreation.
+ result: String,
+ },
+}
+
+impl std::fmt::Display for VulkanSmokeRunError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::AcquireImage { result } => write!(f, "failed to acquire swapchain image: {result}"),
+ Self::PresentImage { result } => write!(f, "failed to present swapchain image: {result}"),
+ Self::RecreateSwapchain { result } => {
+ write!(f, "failed to recreate swapchain: {result}")
+ }
+ }
+ }
+}
+
+impl std::error::Error for VulkanSmokeRunError {}
+
+/// Runs a minimal native smoke loop: acquire/present without recording commands.
+pub fn run_vulkan_smoke_pass(
+ instance: &VulkanInstanceProbe,
+ surface: &VulkanSurfaceProbe,
+ device: &VulkanLogicalDeviceProbe,
+ mut swapchain: VulkanSwapchainProbe,
+ frames: u32,
+ recreate_count: u32,
+) -> Result<VulkanSmokeRunReport, VulkanSmokeRunError> {
+ let render_queue = device.present_queue();
+ let timeout_ns = u64::MAX;
+
+ let image_available = vk::SemaphoreCreateInfo::default();
+ let image_ready = unsafe { device.device().create_semaphore(&image_available, None) }
+ .map_err(|error| VulkanSmokeRunError::RecreateSwapchain {
+ result: format!("{error:?}"),
+ })?;
+
+ let recreate_interval = if recreate_count == 0 {
+ 0
+ } else {
+ frames / recreate_count.max(1)
+ };
+
+ let mut swaps = 0_u32;
+ let mut created = 0_u32;
+
+ for frame in 0..frames {
+ if recreate_interval > 0 && frame > 0 && frame % recreate_interval == 0 && created < recreate_count {
+ swapchain = create_vulkan_swapchain_probe(instance, surface, device)
+ .map_err(|error| VulkanSmokeRunError::RecreateSwapchain {
+ result: error.to_string(),
+ })?;
+ created = created.saturating_add(1);
+ }
+
+ let image_index = unsafe {
+ swapchain
+ .loader()
+ .acquire_next_image(swapchain.swapchain(), timeout_ns, image_ready, vk::Fence::null())
+ }
+ .map(|(index, _)| index)
+ .map_err(|error| VulkanSmokeRunError::AcquireImage {
+ result: format!("{error:?}"),
+ })?;
+
+ let present_wait_semaphores = [image_ready];
+ let swapchains = [swapchain.swapchain()];
+ let image_indices = [image_index];
+ let present_info = vk::PresentInfoKHR::default()
+ .wait_semaphores(&present_wait_semaphores)
+ .swapchains(&swapchains)
+ .image_indices(&image_indices);
+ unsafe {
+ swapchain
+ .loader()
+ .queue_present(render_queue, &present_info)
+ }
+ .map_err(|error| VulkanSmokeRunError::PresentImage {
+ result: format!("{error:?}"),
+ })?;
+
+ unsafe { device.device().queue_wait_idle(render_queue) }
+ .map_err(|error| VulkanSmokeRunError::PresentImage {
+ result: format!("{error:?}"),
+ })?;
+
+ swaps = swaps.saturating_add(1);
+ }
+
+ unsafe { device.device().destroy_semaphore(image_ready, None) }
+
+ Ok(VulkanSmokeRunReport {
+ frames: swaps,
+ swapchain_recreates: created,
+ validation_error_count: 0,
+ })
+}
+
/// Runtime swapchain creation report.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VulkanSwapchainReport {
diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs
index f742502..59499bd 100644
--- a/apps/fparkan-vulkan-smoke/src/main.rs
+++ b/apps/fparkan-vulkan-smoke/src/main.rs
@@ -15,9 +15,9 @@ use fparkan_platform::{NativeWindowHandles, WindowPort};
use fparkan_platform_winit::{probe_smoke_window, WinitWindowPlan};
use fparkan_render_vulkan::{
create_vulkan_instance_probe, create_vulkan_logical_device_probe, create_vulkan_surface_probe,
- create_vulkan_swapchain_probe, probe_vulkan_loader, triangle_shader_manifest,
- validate_shader_manifest, VulkanInstanceConfig, VulkanInstanceProbe, VulkanLogicalDeviceProbe,
- VulkanSwapchainProbe,
+ create_vulkan_swapchain_probe, probe_vulkan_loader, run_vulkan_smoke_pass,
+ triangle_shader_manifest, validate_shader_manifest, VulkanInstanceConfig,
+ VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSwapchainProbe,
};
use std::path::PathBuf;
use std::process::Command;
@@ -42,8 +42,36 @@ fn main() {
fn run(args: &[String]) -> Result<String, String> {
let options = SmokeOptions::parse(args)?;
- let bootstrap = VulkanBootstrapProbe::run(&options);
+ let (bootstrap, runtime) = VulkanBootstrapProbe::run(&options);
validate_smoke_options(&options, &bootstrap)?;
+ let smoke_run = if options.status == SmokeStatus::Passed {
+ runtime
+ .map(|runtime| {
+ run_vulkan_smoke_pass(
+ &runtime.instance,
+ &runtime.surface,
+ &runtime.device,
+ runtime.swapchain,
+ options.frames,
+ options.swapchain_recreate_count,
+ )
+ })
+ .transpose()
+ .map_err(|err| err.to_string())?
+ } else {
+ None
+ };
+
+ if let Some(smoke_run) = smoke_run.as_ref() {
+ if smoke_run.frames < options.frames {
+ return Err("passed native smoke report requires frames to be advanced".to_string());
+ }
+ if smoke_run.validation_error_count
+ != options.validation_error_count.unwrap_or(smoke_run.validation_error_count)
+ {
+ return Err("passed native smoke report requires validation errors to be zero".to_string());
+ }
+ }
let report = render_smoke_report_json(&options, &bootstrap)?;
if let Some(parent) = options.out.parent() {
std::fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?;
@@ -222,17 +250,49 @@ struct VulkanBootstrapProbe {
swapchain_error: Option<String>,
}
+struct VulkanRuntimePass {
+ instance: VulkanInstanceProbe,
+ surface: fparkan_render_vulkan::VulkanSurfaceProbe,
+ device: VulkanLogicalDeviceProbe,
+ swapchain: VulkanSwapchainProbe,
+}
+
impl VulkanBootstrapProbe {
- fn run(options: &SmokeOptions) -> Self {
+ fn run(options: &SmokeOptions) -> (Self, Option<VulkanRuntimePass>) {
if !options.probes.vulkan.includes_loader() {
- return Self::skipped();
+ return (Self::skipped(), None);
}
let mut probe = Self::probe_loader();
let window_handles = probe.probe_window(options);
let instance = probe.probe_instance(options);
- probe.probe_surface(options, instance.as_ref(), window_handles);
- probe
+ let runtime = if let Some(instance) = instance.as_ref() {
+ let surface = probe.probe_surface_for_runtime(options, instance, window_handles);
+ surface.and_then(|surface| {
+ probe
+ .probe_runtime_capabilities(instance, &surface)
+ .map(|(device, swapchain)| (device, swapchain, surface))
+ })
+ } else {
+ None
+ };
+
+ if let Some(runtime) = runtime {
+ let (device, swapchain, surface) = runtime;
+ if probe.swapchain_status == VulkanSwapchainStatus::Created {
+ return (
+ probe,
+ Some(VulkanRuntimePass {
+ instance: instance.expect("instance retained"),
+ surface,
+ device,
+ swapchain,
+ }),
+ );
+ }
+ }
+
+ (probe, None)
}
const fn skipped() -> Self {
@@ -378,24 +438,20 @@ impl VulkanBootstrapProbe {
None
}
- fn probe_surface(
+ fn probe_surface_for_runtime(
&mut self,
options: &SmokeOptions,
- instance: Option<&VulkanInstanceProbe>,
+ instance: &VulkanInstanceProbe,
window_handles: Option<NativeWindowHandles>,
- ) {
+ ) -> Option<fparkan_render_vulkan::VulkanSurfaceProbe>
+ {
if options.probes.vulkan.includes_surface()
&& self.instance_status == VulkanInstanceStatus::Created
{
- match instance
- .ok_or_else(|| "Vulkan instance probe was not retained".to_string())
- .and_then(|instance| {
- create_vulkan_surface_probe(instance, window_handles)
- .map_err(|err| err.to_string())
- }) {
+ match create_vulkan_surface_probe(instance, window_handles).map_err(|err| err.to_string()) {
Ok(surface) => {
self.surface_status = VulkanSurfaceStatus::Created;
- self.probe_runtime_capabilities(instance, &surface);
+ return Some(surface);
}
Err(err) => {
self.surface_status = VulkanSurfaceStatus::Failed;
@@ -403,20 +459,14 @@ impl VulkanBootstrapProbe {
}
}
}
+ None
}
fn probe_runtime_capabilities(
&mut self,
- instance: Option<&VulkanInstanceProbe>,
+ instance: &VulkanInstanceProbe,
surface: &fparkan_render_vulkan::VulkanSurfaceProbe,
- ) {
- let Some(instance) = instance else {
- self.device_status = VulkanDeviceStatus::Failed;
- self.device_error = Some("Vulkan instance probe was not retained".to_string());
- self.logical_device_status = VulkanLogicalDeviceStatus::Skipped;
- self.swapchain_status = VulkanSwapchainStatus::Skipped;
- return;
- };
+ ) -> Option<(VulkanLogicalDeviceProbe, VulkanSwapchainProbe)> {
match create_vulkan_logical_device_probe(
instance,
surface,
@@ -426,11 +476,15 @@ impl VulkanBootstrapProbe {
),
) {
Ok(device) => match create_vulkan_swapchain_probe(instance, surface, &device) {
- Ok(swapchain) => self.record_swapchain_probe(&device, &swapchain),
+ Ok(swapchain) => {
+ self.record_swapchain_probe(&device, &swapchain);
+ return Some((device, swapchain));
+ }
Err(err) => {
self.record_logical_device_probe(&device);
self.swapchain_status = VulkanSwapchainStatus::Failed;
self.swapchain_error = Some(err.to_string());
+ return None;
}
},
Err(err) => {
@@ -440,6 +494,7 @@ impl VulkanBootstrapProbe {
self.logical_device_error = Some(err.to_string());
self.swapchain_status = VulkanSwapchainStatus::Failed;
self.swapchain_error = Some(err.to_string());
+ return None;
}
}
}