feat(vulkan-smoke): run native swapchain acquire/present
This commit is contained in:
@@ -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.
|
/// Logical device creation report.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct VulkanLogicalDeviceReport {
|
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.
|
/// Runtime swapchain creation report.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct VulkanSwapchainReport {
|
pub struct VulkanSwapchainReport {
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ use fparkan_platform::{NativeWindowHandles, WindowPort};
|
|||||||
use fparkan_platform_winit::{probe_smoke_window, WinitWindowPlan};
|
use fparkan_platform_winit::{probe_smoke_window, WinitWindowPlan};
|
||||||
use fparkan_render_vulkan::{
|
use fparkan_render_vulkan::{
|
||||||
create_vulkan_instance_probe, create_vulkan_logical_device_probe, create_vulkan_surface_probe,
|
create_vulkan_instance_probe, create_vulkan_logical_device_probe, create_vulkan_surface_probe,
|
||||||
create_vulkan_swapchain_probe, probe_vulkan_loader, triangle_shader_manifest,
|
create_vulkan_swapchain_probe, probe_vulkan_loader, run_vulkan_smoke_pass,
|
||||||
validate_shader_manifest, VulkanInstanceConfig, VulkanInstanceProbe, VulkanLogicalDeviceProbe,
|
triangle_shader_manifest, validate_shader_manifest, VulkanInstanceConfig,
|
||||||
VulkanSwapchainProbe,
|
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSwapchainProbe,
|
||||||
};
|
};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -42,8 +42,36 @@ fn main() {
|
|||||||
|
|
||||||
fn run(args: &[String]) -> Result<String, String> {
|
fn run(args: &[String]) -> Result<String, String> {
|
||||||
let options = SmokeOptions::parse(args)?;
|
let options = SmokeOptions::parse(args)?;
|
||||||
let bootstrap = VulkanBootstrapProbe::run(&options);
|
let (bootstrap, runtime) = VulkanBootstrapProbe::run(&options);
|
||||||
validate_smoke_options(&options, &bootstrap)?;
|
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)?;
|
let report = render_smoke_report_json(&options, &bootstrap)?;
|
||||||
if let Some(parent) = options.out.parent() {
|
if let Some(parent) = options.out.parent() {
|
||||||
std::fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?;
|
std::fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?;
|
||||||
@@ -222,17 +250,49 @@ struct VulkanBootstrapProbe {
|
|||||||
swapchain_error: Option<String>,
|
swapchain_error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct VulkanRuntimePass {
|
||||||
|
instance: VulkanInstanceProbe,
|
||||||
|
surface: fparkan_render_vulkan::VulkanSurfaceProbe,
|
||||||
|
device: VulkanLogicalDeviceProbe,
|
||||||
|
swapchain: VulkanSwapchainProbe,
|
||||||
|
}
|
||||||
|
|
||||||
impl VulkanBootstrapProbe {
|
impl VulkanBootstrapProbe {
|
||||||
fn run(options: &SmokeOptions) -> Self {
|
fn run(options: &SmokeOptions) -> (Self, Option<VulkanRuntimePass>) {
|
||||||
if !options.probes.vulkan.includes_loader() {
|
if !options.probes.vulkan.includes_loader() {
|
||||||
return Self::skipped();
|
return (Self::skipped(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut probe = Self::probe_loader();
|
let mut probe = Self::probe_loader();
|
||||||
let window_handles = probe.probe_window(options);
|
let window_handles = probe.probe_window(options);
|
||||||
let instance = probe.probe_instance(options);
|
let instance = probe.probe_instance(options);
|
||||||
probe.probe_surface(options, instance.as_ref(), window_handles);
|
let runtime = if let Some(instance) = instance.as_ref() {
|
||||||
probe
|
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 {
|
const fn skipped() -> Self {
|
||||||
@@ -378,24 +438,20 @@ impl VulkanBootstrapProbe {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn probe_surface(
|
fn probe_surface_for_runtime(
|
||||||
&mut self,
|
&mut self,
|
||||||
options: &SmokeOptions,
|
options: &SmokeOptions,
|
||||||
instance: Option<&VulkanInstanceProbe>,
|
instance: &VulkanInstanceProbe,
|
||||||
window_handles: Option<NativeWindowHandles>,
|
window_handles: Option<NativeWindowHandles>,
|
||||||
) {
|
) -> Option<fparkan_render_vulkan::VulkanSurfaceProbe>
|
||||||
|
{
|
||||||
if options.probes.vulkan.includes_surface()
|
if options.probes.vulkan.includes_surface()
|
||||||
&& self.instance_status == VulkanInstanceStatus::Created
|
&& self.instance_status == VulkanInstanceStatus::Created
|
||||||
{
|
{
|
||||||
match instance
|
match create_vulkan_surface_probe(instance, window_handles).map_err(|err| err.to_string()) {
|
||||||
.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())
|
|
||||||
}) {
|
|
||||||
Ok(surface) => {
|
Ok(surface) => {
|
||||||
self.surface_status = VulkanSurfaceStatus::Created;
|
self.surface_status = VulkanSurfaceStatus::Created;
|
||||||
self.probe_runtime_capabilities(instance, &surface);
|
return Some(surface);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.surface_status = VulkanSurfaceStatus::Failed;
|
self.surface_status = VulkanSurfaceStatus::Failed;
|
||||||
@@ -403,20 +459,14 @@ impl VulkanBootstrapProbe {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn probe_runtime_capabilities(
|
fn probe_runtime_capabilities(
|
||||||
&mut self,
|
&mut self,
|
||||||
instance: Option<&VulkanInstanceProbe>,
|
instance: &VulkanInstanceProbe,
|
||||||
surface: &fparkan_render_vulkan::VulkanSurfaceProbe,
|
surface: &fparkan_render_vulkan::VulkanSurfaceProbe,
|
||||||
) {
|
) -> Option<(VulkanLogicalDeviceProbe, VulkanSwapchainProbe)> {
|
||||||
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;
|
|
||||||
};
|
|
||||||
match create_vulkan_logical_device_probe(
|
match create_vulkan_logical_device_probe(
|
||||||
instance,
|
instance,
|
||||||
surface,
|
surface,
|
||||||
@@ -426,11 +476,15 @@ impl VulkanBootstrapProbe {
|
|||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Ok(device) => match create_vulkan_swapchain_probe(instance, surface, &device) {
|
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) => {
|
Err(err) => {
|
||||||
self.record_logical_device_probe(&device);
|
self.record_logical_device_probe(&device);
|
||||||
self.swapchain_status = VulkanSwapchainStatus::Failed;
|
self.swapchain_status = VulkanSwapchainStatus::Failed;
|
||||||
self.swapchain_error = Some(err.to_string());
|
self.swapchain_error = Some(err.to_string());
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -440,6 +494,7 @@ impl VulkanBootstrapProbe {
|
|||||||
self.logical_device_error = Some(err.to_string());
|
self.logical_device_error = Some(err.to_string());
|
||||||
self.swapchain_status = VulkanSwapchainStatus::Failed;
|
self.swapchain_status = VulkanSwapchainStatus::Failed;
|
||||||
self.swapchain_error = Some(err.to_string());
|
self.swapchain_error = Some(err.to_string());
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user