aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-25 05:42:28 +0300
committerValentin Popov <valentin@popov.link>2026-06-25 10:45:36 +0300
commit6a2adbe16048c1e36998b98e41c73cbb64270d26 (patch)
tree1da623aa4a48e8d1431ca347da0dfa5a823fa2c9
parentb8933dd43a7e83fa37201cdb9b1428867c78379b (diff)
downloadfparkan-6a2adbe16048c1e36998b98e41c73cbb64270d26.tar.xz
fparkan-6a2adbe16048c1e36998b98e41c73cbb64270d26.zip
refactor(vulkan-ffi): extract smoke renderer module
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi.rs601
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/resources.rs7
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke.rs612
-rw-r--r--xtask/src/main.rs1
4 files changed, 620 insertions, 601 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs
index b7ec403..c5025a5 100644
--- a/adapters/fparkan-render-vulkan/src/ffi.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi.rs
@@ -30,6 +30,7 @@
mod instance;
mod resources;
mod runtime;
+mod smoke;
mod surface;
mod validation;
@@ -60,9 +61,7 @@ pub use self::surface::{
VulkanSurfacePlan, VulkanSurfaceProbe,
};
use self::validation::{create_validation_messenger, VulkanValidationMessenger};
-use crate::shader_manifest::{
- triangle_shader_manifest, validate_shader_manifest, VulkanShaderManifestError,
-};
+use crate::shader_manifest::VulkanShaderManifestError;
use ash::vk;
use fparkan_platform::NativeWindowHandles;
/// Minimum Vulkan API version accepted by the Stage 0 backend.
@@ -585,602 +584,6 @@ pub struct VulkanSmokeRenderer {
report: VulkanSmokeRendererReport,
}
-impl VulkanSmokeRenderer {
- /// Creates a live Vulkan smoke renderer bound to a live native window.
- ///
- /// # Errors
- ///
- /// Returns [`VulkanSmokeRendererError`] when Vulkan bootstrap, pipeline creation,
- /// memory allocation, or synchronization resource creation fails.
- pub fn new(
- create_info: &VulkanSmokeRendererCreateInfo,
- ) -> Result<Self, VulkanSmokeRendererError> {
- let shader_manifest = validate_shader_manifest(&triangle_shader_manifest())
- .map_err(VulkanSmokeRendererError::ShaderManifest)?;
- let surface_plan = plan_vulkan_surface(Some(create_info.native_handles))
- .map_err(VulkanSmokeRendererError::Surface)?;
- let mut instance_config = VulkanInstanceConfig::smoke(&create_info.application_name);
- instance_config
- .required_extensions
- .clone_from(&surface_plan.required_instance_extensions);
- instance_config.enable_validation = create_info.enable_validation;
- let instance = create_vulkan_instance_probe(&instance_config)
- .map_err(VulkanSmokeRendererError::Instance)?;
- let validation = if create_info.enable_validation {
- Some(create_validation_messenger(&instance)?)
- } else {
- None
- };
- let surface = create_vulkan_surface_probe(&instance, Some(create_info.native_handles))
- .map_err(VulkanSmokeRendererError::Surface)?;
- let device =
- create_vulkan_logical_device_probe(&instance, &surface, create_info.drawable_extent)
- .map_err(VulkanSmokeRendererError::LogicalDevice)?;
- let swapchain = create_vulkan_swapchain_probe_for_extent(
- &instance,
- &surface,
- &device,
- create_info.drawable_extent,
- vk::SwapchainKHR::null(),
- )
- .map_err(VulkanSmokeRendererError::Swapchain)?;
- let command_pool = create_command_pool(&device)?;
- let vertex_buffer = match create_triangle_vertex_buffer(&instance, &device) {
- Ok(buffer) => buffer,
- Err(error) => {
- // SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
- unsafe { device.device().destroy_command_pool(command_pool, None) };
- return Err(error);
- }
- };
- let index_buffer = match create_triangle_index_buffer(&instance, &device) {
- Ok(buffer) => buffer,
- Err(error) => {
- // SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
- unsafe { device.device().destroy_command_pool(command_pool, None) };
- destroy_allocated_buffer(&device, &vertex_buffer);
- return Err(error);
- }
- };
- let mut renderer = Self {
- instance: Some(instance),
- validation,
- surface: Some(surface),
- device: Some(device),
- swapchain: Some(swapchain),
- command_pool,
- swapchain_resources: None,
- vertex_buffer: Some(vertex_buffer),
- index_buffer: Some(index_buffer),
- frame_sync: Vec::new(),
- images_in_flight: Vec::new(),
- current_frame: 0,
- pending_extent: None,
- swapchain_recreate_count: 0,
- report: VulkanSmokeRendererReport {
- shader_manifest_hash: shader_manifest.manifest_hash.clone(),
- portability_enumeration: instance_config.enable_portability_enumeration,
- device_name: String::new(),
- graphics_queue_family: 0,
- present_queue_family: 0,
- enabled_extension_count: 0,
- swapchain_extent: (0, 0),
- swapchain_image_count: 0,
- },
- };
- renderer.rebuild_swapchain_resources(false)?;
- let device_ref = renderer.device_ref()?;
- let swapchain_ref = renderer.swapchain_ref()?;
- renderer.report = VulkanSmokeRendererReport {
- shader_manifest_hash: shader_manifest.manifest_hash,
- portability_enumeration: renderer
- .instance
- .as_ref()
- .is_some_and(|instance| instance.report.create_flags != 0),
- device_name: device_ref.report.device_name.clone(),
- graphics_queue_family: device_ref.report.graphics_queue_family,
- present_queue_family: device_ref.report.present_queue_family,
- enabled_extension_count: device_ref
- .report
- .enabled_extensions
- .len()
- .try_into()
- .unwrap_or(u32::MAX),
- swapchain_extent: swapchain_ref.report.plan.extent,
- swapchain_image_count: swapchain_ref.report.image_count,
- };
- Ok(renderer)
- }
-
- /// Returns the current bootstrap report.
- #[must_use]
- pub const fn report(&self) -> &VulkanSmokeRendererReport {
- &self.report
- }
-
- /// Returns measured validation counters and VUIDs.
- #[must_use]
- pub fn validation_report(&self) -> VulkanValidationReport {
- self.validation.as_ref().map_or(
- VulkanValidationReport {
- warning_count: 0,
- error_count: 0,
- vuids: Vec::new(),
- },
- VulkanValidationMessenger::report,
- )
- }
-
- /// Returns the measured swapchain recreation count.
- #[must_use]
- pub const fn swapchain_recreate_count(&self) -> u32 {
- self.swapchain_recreate_count
- }
-
- /// Requests swapchain recreation for a new drawable extent.
- pub fn request_resize(&mut self, extent: (u32, u32)) {
- self.pending_extent = Some(extent);
- }
-
- fn device_ref(&self) -> Result<&VulkanLogicalDeviceProbe, VulkanSmokeRendererError> {
- self.device
- .as_ref()
- .ok_or(VulkanSmokeRendererError::InvariantViolation {
- context: "logical device",
- })
- }
-
- fn swapchain_ref(&self) -> Result<&VulkanSwapchainProbe, VulkanSmokeRendererError> {
- self.swapchain
- .as_ref()
- .ok_or(VulkanSmokeRendererError::InvariantViolation {
- context: "swapchain",
- })
- }
-
- fn instance_ref(&self) -> Result<&VulkanInstanceProbe, VulkanSmokeRendererError> {
- self.instance
- .as_ref()
- .ok_or(VulkanSmokeRendererError::InvariantViolation {
- context: "instance",
- })
- }
-
- fn surface_ref(&self) -> Result<&VulkanSurfaceProbe, VulkanSmokeRendererError> {
- self.surface
- .as_ref()
- .ok_or(VulkanSmokeRendererError::InvariantViolation { context: "surface" })
- }
-
- fn resources_ref(&self) -> Result<&VulkanSwapchainResources, VulkanSmokeRendererError> {
- self.swapchain_resources
- .as_ref()
- .ok_or(VulkanSmokeRendererError::InvariantViolation {
- context: "swapchain resources",
- })
- }
-
- fn vertex_buffer_ref(&self) -> Result<&VulkanAllocatedBuffer, VulkanSmokeRendererError> {
- self.vertex_buffer
- .as_ref()
- .ok_or(VulkanSmokeRendererError::InvariantViolation {
- context: "vertex buffer",
- })
- }
-
- fn index_buffer_ref(&self) -> Result<&VulkanAllocatedBuffer, VulkanSmokeRendererError> {
- self.index_buffer
- .as_ref()
- .ok_or(VulkanSmokeRendererError::InvariantViolation {
- context: "index buffer",
- })
- }
-
- /// Draws and presents one indexed-triangle frame.
- ///
- /// # Errors
- ///
- /// Returns [`VulkanSmokeRendererError`] when synchronization, command recording,
- /// submission, or presentation fails.
- #[allow(clippy::too_many_lines)]
- pub fn draw_frame(&mut self) -> Result<VulkanSmokeFrameOutcome, VulkanSmokeRendererError> {
- if let Some(extent) = self.pending_extent.take() {
- if extent.0 == 0 || extent.1 == 0 {
- self.pending_extent = Some(extent);
- return Ok(VulkanSmokeFrameOutcome::ZeroExtent);
- }
- self.recreate_swapchain(extent)?;
- return Ok(VulkanSmokeFrameOutcome::Recreated);
- }
-
- let sync = &self.frame_sync[self.current_frame];
- let image_available = sync.image_available;
- let render_finished = sync.render_finished;
- let in_flight_fence = sync.fence;
- // SAFETY: The fence belongs to this live logical device and is waited from one thread.
- unsafe {
- self.device_ref()?
- .device()
- .wait_for_fences(&[in_flight_fence], true, 1_000_000_000)
- }
- .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkWaitForFences",
- result: error,
- })?;
- // SAFETY: The swapchain, semaphore and fence inputs are live for the duration of the acquire call.
- let acquire = unsafe {
- self.swapchain_ref()?.loader().acquire_next_image(
- self.swapchain_ref()?.swapchain(),
- 1_000_000_000,
- image_available,
- vk::Fence::null(),
- )
- };
- let (image_index, acquire_suboptimal) = match acquire {
- Ok(result) => result,
- Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
- self.recreate_swapchain(self.report.swapchain_extent)?;
- return Ok(VulkanSmokeFrameOutcome::Recreated);
- }
- Err(error) => {
- return Err(VulkanSmokeRendererError::VulkanOperation {
- context: "vkAcquireNextImageKHR",
- result: error,
- });
- }
- };
- let image_index_usize = usize::try_from(image_index).unwrap_or(0);
- let image_fence = self.images_in_flight[image_index_usize];
- if image_fence != vk::Fence::null() {
- // SAFETY: The fence belongs to this renderer and can be waited independently.
- unsafe {
- self.device_ref()?
- .device()
- .wait_for_fences(&[image_fence], true, 1_000_000_000)
- }
- .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkWaitForFences(image)",
- result: error,
- })?;
- }
- self.images_in_flight[image_index_usize] = in_flight_fence;
- // SAFETY: The fence belongs to this frame context and is not in use after the wait above.
- unsafe { self.device_ref()?.device().reset_fences(&[in_flight_fence]) }.map_err(
- |error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkResetFences",
- result: error,
- },
- )?;
-
- self.record_command_buffer(image_index_usize)?;
- let wait_semaphores = [image_available];
- let wait_stages = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT];
- let command_buffers = [self.resources_ref()?.command_buffers[image_index_usize]];
- let signal_semaphores = [render_finished];
- let submit_info = [vk::SubmitInfo::default()
- .wait_semaphores(&wait_semaphores)
- .wait_dst_stage_mask(&wait_stages)
- .command_buffers(&command_buffers)
- .signal_semaphores(&signal_semaphores)];
- // SAFETY: Submission references live queue, sync objects and recorded command buffer.
- unsafe {
- self.device_ref()?.device().queue_submit(
- self.device_ref()?.graphics_queue(),
- &submit_info,
- in_flight_fence,
- )
- }
- .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkQueueSubmit",
- result: error,
- })?;
-
- let present_wait = [render_finished];
- let swapchains = [self.swapchain_ref()?.swapchain()];
- let image_indices = [image_index];
- let present_info = vk::PresentInfoKHR::default()
- .wait_semaphores(&present_wait)
- .swapchains(&swapchains)
- .image_indices(&image_indices);
- // SAFETY: Presentation uses the rendered image index and a semaphore signaled by queue submission.
- let present_suboptimal = match unsafe {
- self.swapchain_ref()?
- .loader()
- .queue_present(self.device_ref()?.present_queue(), &present_info)
- } {
- Ok(suboptimal) => suboptimal,
- Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
- self.recreate_swapchain(self.report.swapchain_extent)?;
- return Ok(VulkanSmokeFrameOutcome::Recreated);
- }
- Err(error) => {
- return Err(VulkanSmokeRendererError::VulkanOperation {
- context: "vkQueuePresentKHR",
- result: error,
- });
- }
- };
-
- self.current_frame = (self.current_frame + 1) % self.frame_sync.len().max(1);
- if acquire_suboptimal || present_suboptimal {
- self.recreate_swapchain(self.report.swapchain_extent)?;
- Ok(VulkanSmokeFrameOutcome::Recreated)
- } else {
- Ok(VulkanSmokeFrameOutcome::Presented)
- }
- }
-
- fn recreate_swapchain(&mut self, extent: (u32, u32)) -> Result<(), VulkanSmokeRendererError> {
- let device = self.device_ref()?;
- // SAFETY: The logical device remains live and idling at swapchain recreation boundaries.
- unsafe { device.device().device_wait_idle() }.map_err(|error| {
- VulkanSmokeRendererError::VulkanOperation {
- context: "vkDeviceWaitIdle",
- result: error,
- }
- })?;
- self.pending_extent = None;
- self.rebuild_swapchain(extent)?;
- self.swapchain_recreate_count = self.swapchain_recreate_count.saturating_add(1);
- Ok(())
- }
-
- fn rebuild_swapchain(&mut self, extent: (u32, u32)) -> Result<(), VulkanSmokeRendererError> {
- self.destroy_swapchain_resources();
- let instance = self.instance_ref()?;
- let surface = self.surface_ref()?;
- let device = self.device_ref()?;
- let old_swapchain = self
- .swapchain
- .as_ref()
- .map_or(vk::SwapchainKHR::null(), VulkanSwapchainProbe::swapchain);
- let new_swapchain = create_vulkan_swapchain_probe_for_extent(
- instance,
- surface,
- device,
- extent,
- old_swapchain,
- )
- .map_err(VulkanSmokeRendererError::Swapchain)?;
- self.swapchain = Some(new_swapchain);
- self.rebuild_swapchain_resources(true)?;
- Ok(())
- }
-
- fn rebuild_swapchain_resources(
- &mut self,
- reuse_command_pool: bool,
- ) -> Result<(), VulkanSmokeRendererError> {
- let resources = {
- let device = self.device_ref()?;
- let swapchain = self.swapchain_ref()?;
- create_swapchain_resources(
- device,
- swapchain,
- self.command_pool,
- self.vertex_buffer_ref()?,
- self.index_buffer_ref()?,
- reuse_command_pool,
- )?
- };
- let frame_sync = {
- let device = self.device_ref()?;
- create_frame_sync(device)?
- };
- let swapchain_extent = self.swapchain_ref()?.report.plan.extent;
- let swapchain_image_count = self.swapchain_ref()?.report.image_count;
- self.images_in_flight = vec![vk::Fence::null(); resources.image_views.len()];
- self.frame_sync = frame_sync;
- self.report.swapchain_extent = swapchain_extent;
- self.report.swapchain_image_count = swapchain_image_count;
- self.swapchain_resources = Some(resources);
- Ok(())
- }
-
- #[allow(clippy::too_many_lines)]
- fn record_command_buffer(
- &mut self,
- image_index: usize,
- ) -> Result<(), VulkanSmokeRendererError> {
- let device = self.device_ref()?;
- let swapchain = self.swapchain_ref()?;
- let resources = self.resources_ref()?;
- let command_buffer = resources.command_buffers[image_index];
- // SAFETY: The command buffer belongs to the resettable pool owned by this renderer.
- unsafe {
- device
- .device()
- .reset_command_buffer(command_buffer, vk::CommandBufferResetFlags::empty())
- }
- .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkResetCommandBuffer",
- result: error,
- })?;
- let begin_info = vk::CommandBufferBeginInfo::default();
- // SAFETY: The command buffer is in the initial state after reset and recorded on one thread.
- unsafe {
- device
- .device()
- .begin_command_buffer(command_buffer, &begin_info)
- }
- .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkBeginCommandBuffer",
- result: error,
- })?;
-
- let pre_barrier = vk::ImageMemoryBarrier::default()
- .old_layout(vk::ImageLayout::PRESENT_SRC_KHR)
- .new_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
- .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
- .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
- .subresource_range(color_subresource_range())
- .src_access_mask(vk::AccessFlags::empty())
- .dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE);
- // SAFETY: The swapchain is live and queried only to resolve the current image handles.
- let swapchain_images = unsafe {
- swapchain
- .loader()
- .get_swapchain_images(swapchain.swapchain())
- }
- .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkGetSwapchainImagesKHR",
- result: error,
- })?;
- let pre_barrier = pre_barrier.image(swapchain_images[image_index]);
- // SAFETY: The barriers operate on the acquired swapchain image owned by this command buffer submission.
- unsafe {
- device.device().cmd_pipeline_barrier(
- command_buffer,
- vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
- vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
- vk::DependencyFlags::empty(),
- &[],
- &[],
- &[pre_barrier],
- );
- }
-
- let clear_values = [vk::ClearValue {
- color: vk::ClearColorValue {
- float32: [0.05, 0.08, 0.11, 1.0],
- },
- }];
- let render_area = vk::Rect2D {
- offset: vk::Offset2D { x: 0, y: 0 },
- extent: vk::Extent2D {
- width: swapchain.report.plan.extent.0,
- height: swapchain.report.plan.extent.1,
- },
- };
- let render_pass_info = vk::RenderPassBeginInfo::default()
- .render_pass(resources.render_pass)
- .framebuffer(resources.framebuffers[image_index])
- .render_area(render_area)
- .clear_values(&clear_values);
- // SAFETY: All commands target live frame resources owned by this renderer.
- unsafe {
- device.device().cmd_begin_render_pass(
- command_buffer,
- &render_pass_info,
- vk::SubpassContents::INLINE,
- );
- device.device().cmd_bind_pipeline(
- command_buffer,
- vk::PipelineBindPoint::GRAPHICS,
- resources.pipeline,
- );
- let vertex_buffers = [self.vertex_buffer_ref()?.buffer];
- let offsets = [0_u64];
- device
- .device()
- .cmd_bind_vertex_buffers(command_buffer, 0, &vertex_buffers, &offsets);
- device.device().cmd_bind_index_buffer(
- command_buffer,
- self.index_buffer_ref()?.buffer,
- 0,
- vk::IndexType::UINT16,
- );
- device
- .device()
- .cmd_draw_indexed(command_buffer, 3, 1, 0, 0, 0);
- device.device().cmd_end_render_pass(command_buffer);
- }
-
- let post_barrier = vk::ImageMemoryBarrier::default()
- .old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
- .new_layout(vk::ImageLayout::PRESENT_SRC_KHR)
- .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
- .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
- .image(swapchain_images[image_index])
- .subresource_range(color_subresource_range())
- .src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
- .dst_access_mask(vk::AccessFlags::empty());
- // SAFETY: The post-render barrier transitions the same live swapchain image into present layout.
- unsafe {
- device.device().cmd_pipeline_barrier(
- command_buffer,
- vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
- vk::PipelineStageFlags::BOTTOM_OF_PIPE,
- vk::DependencyFlags::empty(),
- &[],
- &[],
- &[post_barrier],
- );
- device.device().end_command_buffer(command_buffer)
- }
- .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkEndCommandBuffer",
- result: error,
- })?;
- Ok(())
- }
-
- fn destroy_swapchain_resources(&mut self) {
- let Some(device) = self.device.as_ref() else {
- return;
- };
- for sync in self.frame_sync.drain(..) {
- // SAFETY: These sync objects belong to this device and are destroyed once.
- unsafe {
- device
- .device()
- .destroy_semaphore(sync.image_available, None);
- device
- .device()
- .destroy_semaphore(sync.render_finished, None);
- device.device().destroy_fence(sync.fence, None);
- }
- }
- if let Some(resources) = self.swapchain_resources.take() {
- destroy_swapchain_resources(device, self.command_pool, resources);
- }
- self.images_in_flight.clear();
- self.current_frame = 0;
- }
-
- fn teardown(&mut self) {
- if let Some(device) = self.device.as_ref() {
- // SAFETY: The logical device remains live until teardown finishes and idling prevents in-flight work from touching swapchain, buffers, sync objects or the command pool after destruction starts.
- let _ = unsafe { device.device().device_wait_idle() };
- }
- self.destroy_swapchain_resources();
- if let Some(device) = self.device.as_ref() {
- if let Some(buffer) = self.index_buffer.take() {
- // SAFETY: Buffer and memory belong to this device and are destroyed once after the device has been idled and frame work has been torn down.
- unsafe {
- device.device().destroy_buffer(buffer.buffer, None);
- device.device().free_memory(buffer.memory, None);
- }
- }
- if let Some(buffer) = self.vertex_buffer.take() {
- // SAFETY: Buffer and memory belong to this device and are destroyed once after the device has been idled and frame work has been torn down.
- unsafe {
- device.device().destroy_buffer(buffer.buffer, None);
- device.device().free_memory(buffer.memory, None);
- }
- }
- // SAFETY: The command pool belongs to this device and is destroyed once after the device is idle and all command buffers allocated from it were freed above.
- unsafe {
- device
- .device()
- .destroy_command_pool(self.command_pool, None);
- };
- }
- // Drop child Vulkan owners explicitly before their parents instead of relying on field order.
- self.swapchain.take();
- self.device.take();
- self.surface.take();
- self.validation.take();
- self.instance.take();
- }
-}
-
-impl Drop for VulkanSmokeRenderer {
- fn drop(&mut self) {
- self.teardown();
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
diff --git a/adapters/fparkan-render-vulkan/src/ffi/resources.rs b/adapters/fparkan-render-vulkan/src/ffi/resources.rs
index 6bfe459..4d061c8 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/resources.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/resources.rs
@@ -568,7 +568,8 @@ pub(super) fn create_frame_sync(
context: "vkCreateSemaphore(image_available)",
result: error,
})?;
- let render_finished =
+ let render_finished = {
+ // SAFETY: The sync objects belong to this live logical device and are destroyed at teardown.
match unsafe { device.device().create_semaphore(&semaphore_info, None) } {
Ok(render_finished) => render_finished,
Err(error) => {
@@ -580,7 +581,9 @@ pub(super) fn create_frame_sync(
result: error,
});
}
- };
+ }
+ };
+ // SAFETY: The fence belongs to this live logical device and is destroyed at teardown.
let fence = match unsafe { device.device().create_fence(&fence_info, None) } {
Ok(fence) => fence,
Err(error) => {
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
new file mode 100644
index 0000000..acd52d8
--- /dev/null
+++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
@@ -0,0 +1,612 @@
+#![allow(unsafe_code)]
+
+use ash::vk;
+
+use super::{
+ color_subresource_range, create_command_pool, create_frame_sync, create_swapchain_resources,
+ create_triangle_index_buffer, create_triangle_vertex_buffer, create_validation_messenger,
+ create_vulkan_instance_probe, create_vulkan_logical_device_probe, create_vulkan_surface_probe,
+ create_vulkan_swapchain_probe_for_extent, destroy_allocated_buffer,
+ destroy_swapchain_resources, plan_vulkan_surface, VulkanAllocatedBuffer, VulkanInstanceConfig,
+ VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeFrameOutcome, VulkanSmokeRenderer,
+ VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError, VulkanSmokeRendererReport,
+ VulkanSurfaceProbe, VulkanSwapchainProbe, VulkanSwapchainResources, VulkanValidationMessenger,
+ VulkanValidationReport,
+};
+use crate::shader_manifest::{triangle_shader_manifest, validate_shader_manifest};
+
+impl VulkanSmokeRenderer {
+ /// Creates a live Vulkan smoke renderer bound to a live native window.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`VulkanSmokeRendererError`] when Vulkan bootstrap, pipeline creation,
+ /// memory allocation, or synchronization resource creation fails.
+ pub fn new(
+ create_info: &VulkanSmokeRendererCreateInfo,
+ ) -> Result<Self, VulkanSmokeRendererError> {
+ let shader_manifest = validate_shader_manifest(&triangle_shader_manifest())
+ .map_err(VulkanSmokeRendererError::ShaderManifest)?;
+ let surface_plan = plan_vulkan_surface(Some(create_info.native_handles))
+ .map_err(VulkanSmokeRendererError::Surface)?;
+ let mut instance_config = VulkanInstanceConfig::smoke(&create_info.application_name);
+ instance_config
+ .required_extensions
+ .clone_from(&surface_plan.required_instance_extensions);
+ instance_config.enable_validation = create_info.enable_validation;
+ let instance = create_vulkan_instance_probe(&instance_config)
+ .map_err(VulkanSmokeRendererError::Instance)?;
+ let validation = if create_info.enable_validation {
+ Some(create_validation_messenger(&instance)?)
+ } else {
+ None
+ };
+ let surface = create_vulkan_surface_probe(&instance, Some(create_info.native_handles))
+ .map_err(VulkanSmokeRendererError::Surface)?;
+ let device =
+ create_vulkan_logical_device_probe(&instance, &surface, create_info.drawable_extent)
+ .map_err(VulkanSmokeRendererError::LogicalDevice)?;
+ let swapchain = create_vulkan_swapchain_probe_for_extent(
+ &instance,
+ &surface,
+ &device,
+ create_info.drawable_extent,
+ vk::SwapchainKHR::null(),
+ )
+ .map_err(VulkanSmokeRendererError::Swapchain)?;
+ let command_pool = create_command_pool(&device)?;
+ let vertex_buffer = match create_triangle_vertex_buffer(&instance, &device) {
+ Ok(buffer) => buffer,
+ Err(error) => {
+ // SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
+ unsafe { device.device().destroy_command_pool(command_pool, None) };
+ return Err(error);
+ }
+ };
+ let index_buffer = match create_triangle_index_buffer(&instance, &device) {
+ Ok(buffer) => buffer,
+ Err(error) => {
+ // SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
+ unsafe { device.device().destroy_command_pool(command_pool, None) };
+ destroy_allocated_buffer(&device, &vertex_buffer);
+ return Err(error);
+ }
+ };
+ let mut renderer = Self {
+ instance: Some(instance),
+ validation,
+ surface: Some(surface),
+ device: Some(device),
+ swapchain: Some(swapchain),
+ command_pool,
+ swapchain_resources: None,
+ vertex_buffer: Some(vertex_buffer),
+ index_buffer: Some(index_buffer),
+ frame_sync: Vec::new(),
+ images_in_flight: Vec::new(),
+ current_frame: 0,
+ pending_extent: None,
+ swapchain_recreate_count: 0,
+ report: VulkanSmokeRendererReport {
+ shader_manifest_hash: shader_manifest.manifest_hash.clone(),
+ portability_enumeration: instance_config.enable_portability_enumeration,
+ device_name: String::new(),
+ graphics_queue_family: 0,
+ present_queue_family: 0,
+ enabled_extension_count: 0,
+ swapchain_extent: (0, 0),
+ swapchain_image_count: 0,
+ },
+ };
+ renderer.rebuild_swapchain_resources(false)?;
+ let device_ref = renderer.device_ref()?;
+ let swapchain_ref = renderer.swapchain_ref()?;
+ renderer.report = VulkanSmokeRendererReport {
+ shader_manifest_hash: shader_manifest.manifest_hash,
+ portability_enumeration: renderer
+ .instance
+ .as_ref()
+ .is_some_and(|instance| instance.report.create_flags != 0),
+ device_name: device_ref.report.device_name.clone(),
+ graphics_queue_family: device_ref.report.graphics_queue_family,
+ present_queue_family: device_ref.report.present_queue_family,
+ enabled_extension_count: device_ref
+ .report
+ .enabled_extensions
+ .len()
+ .try_into()
+ .unwrap_or(u32::MAX),
+ swapchain_extent: swapchain_ref.report.plan.extent,
+ swapchain_image_count: swapchain_ref.report.image_count,
+ };
+ Ok(renderer)
+ }
+
+ /// Returns the current bootstrap report.
+ #[must_use]
+ pub const fn report(&self) -> &VulkanSmokeRendererReport {
+ &self.report
+ }
+
+ /// Returns measured validation counters and VUIDs.
+ #[must_use]
+ pub fn validation_report(&self) -> VulkanValidationReport {
+ self.validation.as_ref().map_or(
+ VulkanValidationReport {
+ warning_count: 0,
+ error_count: 0,
+ vuids: Vec::new(),
+ },
+ VulkanValidationMessenger::report,
+ )
+ }
+
+ /// Returns the measured swapchain recreation count.
+ #[must_use]
+ pub const fn swapchain_recreate_count(&self) -> u32 {
+ self.swapchain_recreate_count
+ }
+
+ /// Requests swapchain recreation for a new drawable extent.
+ pub fn request_resize(&mut self, extent: (u32, u32)) {
+ self.pending_extent = Some(extent);
+ }
+
+ fn device_ref(&self) -> Result<&VulkanLogicalDeviceProbe, VulkanSmokeRendererError> {
+ self.device
+ .as_ref()
+ .ok_or(VulkanSmokeRendererError::InvariantViolation {
+ context: "logical device",
+ })
+ }
+
+ fn swapchain_ref(&self) -> Result<&VulkanSwapchainProbe, VulkanSmokeRendererError> {
+ self.swapchain
+ .as_ref()
+ .ok_or(VulkanSmokeRendererError::InvariantViolation {
+ context: "swapchain",
+ })
+ }
+
+ fn instance_ref(&self) -> Result<&VulkanInstanceProbe, VulkanSmokeRendererError> {
+ self.instance
+ .as_ref()
+ .ok_or(VulkanSmokeRendererError::InvariantViolation {
+ context: "instance",
+ })
+ }
+
+ fn surface_ref(&self) -> Result<&VulkanSurfaceProbe, VulkanSmokeRendererError> {
+ self.surface
+ .as_ref()
+ .ok_or(VulkanSmokeRendererError::InvariantViolation { context: "surface" })
+ }
+
+ fn resources_ref(&self) -> Result<&VulkanSwapchainResources, VulkanSmokeRendererError> {
+ self.swapchain_resources
+ .as_ref()
+ .ok_or(VulkanSmokeRendererError::InvariantViolation {
+ context: "swapchain resources",
+ })
+ }
+
+ fn vertex_buffer_ref(&self) -> Result<&VulkanAllocatedBuffer, VulkanSmokeRendererError> {
+ self.vertex_buffer
+ .as_ref()
+ .ok_or(VulkanSmokeRendererError::InvariantViolation {
+ context: "vertex buffer",
+ })
+ }
+
+ fn index_buffer_ref(&self) -> Result<&VulkanAllocatedBuffer, VulkanSmokeRendererError> {
+ self.index_buffer
+ .as_ref()
+ .ok_or(VulkanSmokeRendererError::InvariantViolation {
+ context: "index buffer",
+ })
+ }
+
+ /// Draws and presents one indexed-triangle frame.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`VulkanSmokeRendererError`] when synchronization, command recording,
+ /// submission, or presentation fails.
+ #[allow(clippy::too_many_lines)]
+ pub fn draw_frame(&mut self) -> Result<VulkanSmokeFrameOutcome, VulkanSmokeRendererError> {
+ if let Some(extent) = self.pending_extent.take() {
+ if extent.0 == 0 || extent.1 == 0 {
+ self.pending_extent = Some(extent);
+ return Ok(VulkanSmokeFrameOutcome::ZeroExtent);
+ }
+ self.recreate_swapchain(extent)?;
+ return Ok(VulkanSmokeFrameOutcome::Recreated);
+ }
+
+ let sync = &self.frame_sync[self.current_frame];
+ let image_available = sync.image_available;
+ let render_finished = sync.render_finished;
+ let in_flight_fence = sync.fence;
+ // SAFETY: The fence belongs to this live logical device and is waited from one thread.
+ unsafe {
+ self.device_ref()?
+ .device()
+ .wait_for_fences(&[in_flight_fence], true, 1_000_000_000)
+ }
+ .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkWaitForFences",
+ result: error,
+ })?;
+ // SAFETY: The swapchain, semaphore and fence inputs are live for the duration of the acquire call.
+ let acquire = unsafe {
+ self.swapchain_ref()?.loader().acquire_next_image(
+ self.swapchain_ref()?.swapchain(),
+ 1_000_000_000,
+ image_available,
+ vk::Fence::null(),
+ )
+ };
+ let (image_index, acquire_suboptimal) = match acquire {
+ Ok(result) => result,
+ Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
+ self.recreate_swapchain(self.report.swapchain_extent)?;
+ return Ok(VulkanSmokeFrameOutcome::Recreated);
+ }
+ Err(error) => {
+ return Err(VulkanSmokeRendererError::VulkanOperation {
+ context: "vkAcquireNextImageKHR",
+ result: error,
+ });
+ }
+ };
+ let image_index_usize = usize::try_from(image_index).unwrap_or(0);
+ let image_fence = self.images_in_flight[image_index_usize];
+ if image_fence != vk::Fence::null() {
+ // SAFETY: The fence belongs to this renderer and can be waited independently.
+ unsafe {
+ self.device_ref()?
+ .device()
+ .wait_for_fences(&[image_fence], true, 1_000_000_000)
+ }
+ .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkWaitForFences(image)",
+ result: error,
+ })?;
+ }
+ self.images_in_flight[image_index_usize] = in_flight_fence;
+ // SAFETY: The fence belongs to this frame context and is not in use after the wait above.
+ unsafe { self.device_ref()?.device().reset_fences(&[in_flight_fence]) }.map_err(
+ |error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkResetFences",
+ result: error,
+ },
+ )?;
+
+ self.record_command_buffer(image_index_usize)?;
+ let wait_semaphores = [image_available];
+ let wait_stages = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT];
+ let command_buffers = [self.resources_ref()?.command_buffers[image_index_usize]];
+ let signal_semaphores = [render_finished];
+ let submit_info = [vk::SubmitInfo::default()
+ .wait_semaphores(&wait_semaphores)
+ .wait_dst_stage_mask(&wait_stages)
+ .command_buffers(&command_buffers)
+ .signal_semaphores(&signal_semaphores)];
+ // SAFETY: Submission references live queue, sync objects and recorded command buffer.
+ unsafe {
+ self.device_ref()?.device().queue_submit(
+ self.device_ref()?.graphics_queue(),
+ &submit_info,
+ in_flight_fence,
+ )
+ }
+ .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkQueueSubmit",
+ result: error,
+ })?;
+
+ let present_wait = [render_finished];
+ let swapchains = [self.swapchain_ref()?.swapchain()];
+ let image_indices = [image_index];
+ let present_info = vk::PresentInfoKHR::default()
+ .wait_semaphores(&present_wait)
+ .swapchains(&swapchains)
+ .image_indices(&image_indices);
+ // SAFETY: Presentation uses the rendered image index and a semaphore signaled by queue submission.
+ let present_suboptimal = match unsafe {
+ self.swapchain_ref()?
+ .loader()
+ .queue_present(self.device_ref()?.present_queue(), &present_info)
+ } {
+ Ok(suboptimal) => suboptimal,
+ Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
+ self.recreate_swapchain(self.report.swapchain_extent)?;
+ return Ok(VulkanSmokeFrameOutcome::Recreated);
+ }
+ Err(error) => {
+ return Err(VulkanSmokeRendererError::VulkanOperation {
+ context: "vkQueuePresentKHR",
+ result: error,
+ });
+ }
+ };
+
+ self.current_frame = (self.current_frame + 1) % self.frame_sync.len().max(1);
+ if acquire_suboptimal || present_suboptimal {
+ self.recreate_swapchain(self.report.swapchain_extent)?;
+ Ok(VulkanSmokeFrameOutcome::Recreated)
+ } else {
+ Ok(VulkanSmokeFrameOutcome::Presented)
+ }
+ }
+
+ fn recreate_swapchain(&mut self, extent: (u32, u32)) -> Result<(), VulkanSmokeRendererError> {
+ let device = self.device_ref()?;
+ // SAFETY: The logical device remains live and idling at swapchain recreation boundaries.
+ unsafe { device.device().device_wait_idle() }.map_err(|error| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkDeviceWaitIdle",
+ result: error,
+ }
+ })?;
+ self.pending_extent = None;
+ self.rebuild_swapchain(extent)?;
+ self.swapchain_recreate_count = self.swapchain_recreate_count.saturating_add(1);
+ Ok(())
+ }
+
+ fn rebuild_swapchain(&mut self, extent: (u32, u32)) -> Result<(), VulkanSmokeRendererError> {
+ self.destroy_swapchain_resources();
+ let instance = self.instance_ref()?;
+ let surface = self.surface_ref()?;
+ let device = self.device_ref()?;
+ let old_swapchain = self
+ .swapchain
+ .as_ref()
+ .map_or(vk::SwapchainKHR::null(), VulkanSwapchainProbe::swapchain);
+ let new_swapchain = create_vulkan_swapchain_probe_for_extent(
+ instance,
+ surface,
+ device,
+ extent,
+ old_swapchain,
+ )
+ .map_err(VulkanSmokeRendererError::Swapchain)?;
+ self.swapchain = Some(new_swapchain);
+ self.rebuild_swapchain_resources(true)?;
+ Ok(())
+ }
+
+ fn rebuild_swapchain_resources(
+ &mut self,
+ reuse_command_pool: bool,
+ ) -> Result<(), VulkanSmokeRendererError> {
+ let resources = {
+ let device = self.device_ref()?;
+ let swapchain = self.swapchain_ref()?;
+ create_swapchain_resources(
+ device,
+ swapchain,
+ self.command_pool,
+ self.vertex_buffer_ref()?,
+ self.index_buffer_ref()?,
+ reuse_command_pool,
+ )?
+ };
+ let frame_sync = {
+ let device = self.device_ref()?;
+ create_frame_sync(device)?
+ };
+ let swapchain_extent = self.swapchain_ref()?.report.plan.extent;
+ let swapchain_image_count = self.swapchain_ref()?.report.image_count;
+ self.images_in_flight = vec![vk::Fence::null(); resources.image_views.len()];
+ self.frame_sync = frame_sync;
+ self.report.swapchain_extent = swapchain_extent;
+ self.report.swapchain_image_count = swapchain_image_count;
+ self.swapchain_resources = Some(resources);
+ Ok(())
+ }
+
+ #[allow(clippy::too_many_lines)]
+ fn record_command_buffer(
+ &mut self,
+ image_index: usize,
+ ) -> Result<(), VulkanSmokeRendererError> {
+ let device = self.device_ref()?;
+ let swapchain = self.swapchain_ref()?;
+ let resources = self.resources_ref()?;
+ let command_buffer = resources.command_buffers[image_index];
+ // SAFETY: The command buffer belongs to the resettable pool owned by this renderer.
+ unsafe {
+ device
+ .device()
+ .reset_command_buffer(command_buffer, vk::CommandBufferResetFlags::empty())
+ }
+ .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkResetCommandBuffer",
+ result: error,
+ })?;
+ let begin_info = vk::CommandBufferBeginInfo::default();
+ // SAFETY: The command buffer is in the initial state after reset and recorded on one thread.
+ unsafe {
+ device
+ .device()
+ .begin_command_buffer(command_buffer, &begin_info)
+ }
+ .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkBeginCommandBuffer",
+ result: error,
+ })?;
+
+ let pre_barrier = vk::ImageMemoryBarrier::default()
+ .old_layout(vk::ImageLayout::PRESENT_SRC_KHR)
+ .new_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
+ .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
+ .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
+ .subresource_range(color_subresource_range())
+ .src_access_mask(vk::AccessFlags::empty())
+ .dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE);
+ // SAFETY: The swapchain is live and queried only to resolve the current image handles.
+ let swapchain_images = unsafe {
+ swapchain
+ .loader()
+ .get_swapchain_images(swapchain.swapchain())
+ }
+ .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkGetSwapchainImagesKHR",
+ result: error,
+ })?;
+ let pre_barrier = pre_barrier.image(swapchain_images[image_index]);
+ // SAFETY: The barriers operate on the acquired swapchain image owned by this command buffer submission.
+ unsafe {
+ device.device().cmd_pipeline_barrier(
+ command_buffer,
+ vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
+ vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
+ vk::DependencyFlags::empty(),
+ &[],
+ &[],
+ &[pre_barrier],
+ );
+ }
+
+ let clear_values = [vk::ClearValue {
+ color: vk::ClearColorValue {
+ float32: [0.05, 0.08, 0.11, 1.0],
+ },
+ }];
+ let render_area = vk::Rect2D {
+ offset: vk::Offset2D { x: 0, y: 0 },
+ extent: vk::Extent2D {
+ width: swapchain.report.plan.extent.0,
+ height: swapchain.report.plan.extent.1,
+ },
+ };
+ let render_pass_info = vk::RenderPassBeginInfo::default()
+ .render_pass(resources.render_pass)
+ .framebuffer(resources.framebuffers[image_index])
+ .render_area(render_area)
+ .clear_values(&clear_values);
+ // SAFETY: All commands target live frame resources owned by this renderer.
+ unsafe {
+ device.device().cmd_begin_render_pass(
+ command_buffer,
+ &render_pass_info,
+ vk::SubpassContents::INLINE,
+ );
+ device.device().cmd_bind_pipeline(
+ command_buffer,
+ vk::PipelineBindPoint::GRAPHICS,
+ resources.pipeline,
+ );
+ let vertex_buffers = [self.vertex_buffer_ref()?.buffer];
+ let offsets = [0_u64];
+ device
+ .device()
+ .cmd_bind_vertex_buffers(command_buffer, 0, &vertex_buffers, &offsets);
+ device.device().cmd_bind_index_buffer(
+ command_buffer,
+ self.index_buffer_ref()?.buffer,
+ 0,
+ vk::IndexType::UINT16,
+ );
+ device
+ .device()
+ .cmd_draw_indexed(command_buffer, 3, 1, 0, 0, 0);
+ device.device().cmd_end_render_pass(command_buffer);
+ }
+
+ let post_barrier = vk::ImageMemoryBarrier::default()
+ .old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
+ .new_layout(vk::ImageLayout::PRESENT_SRC_KHR)
+ .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
+ .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
+ .image(swapchain_images[image_index])
+ .subresource_range(color_subresource_range())
+ .src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
+ .dst_access_mask(vk::AccessFlags::empty());
+ // SAFETY: The post-render barrier transitions the same live swapchain image into present layout.
+ unsafe {
+ device.device().cmd_pipeline_barrier(
+ command_buffer,
+ vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
+ vk::PipelineStageFlags::BOTTOM_OF_PIPE,
+ vk::DependencyFlags::empty(),
+ &[],
+ &[],
+ &[post_barrier],
+ );
+ device.device().end_command_buffer(command_buffer)
+ }
+ .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkEndCommandBuffer",
+ result: error,
+ })?;
+ Ok(())
+ }
+
+ fn destroy_swapchain_resources(&mut self) {
+ let Some(device) = self.device.as_ref() else {
+ return;
+ };
+ for sync in self.frame_sync.drain(..) {
+ // SAFETY: These sync objects belong to this device and are destroyed once.
+ unsafe {
+ device
+ .device()
+ .destroy_semaphore(sync.image_available, None);
+ device
+ .device()
+ .destroy_semaphore(sync.render_finished, None);
+ device.device().destroy_fence(sync.fence, None);
+ }
+ }
+ if let Some(resources) = self.swapchain_resources.take() {
+ destroy_swapchain_resources(device, self.command_pool, resources);
+ }
+ self.images_in_flight.clear();
+ self.current_frame = 0;
+ }
+
+ fn teardown(&mut self) {
+ if let Some(device) = self.device.as_ref() {
+ // SAFETY: The logical device remains live until teardown finishes and idling prevents in-flight work from touching swapchain, buffers, sync objects or the command pool after destruction starts.
+ let _ = unsafe { device.device().device_wait_idle() };
+ }
+ self.destroy_swapchain_resources();
+ if let Some(device) = self.device.as_ref() {
+ if let Some(buffer) = self.index_buffer.take() {
+ // SAFETY: Buffer and memory belong to this device and are destroyed once after the device has been idled and frame work has been torn down.
+ unsafe {
+ device.device().destroy_buffer(buffer.buffer, None);
+ device.device().free_memory(buffer.memory, None);
+ }
+ }
+ if let Some(buffer) = self.vertex_buffer.take() {
+ // SAFETY: Buffer and memory belong to this device and are destroyed once after the device has been idled and frame work has been torn down.
+ unsafe {
+ device.device().destroy_buffer(buffer.buffer, None);
+ device.device().free_memory(buffer.memory, None);
+ }
+ }
+ // SAFETY: The command pool belongs to this device and is destroyed once after the device is idle and all command buffers allocated from it were freed above.
+ unsafe {
+ device
+ .device()
+ .destroy_command_pool(self.command_pool, None);
+ };
+ }
+ // Drop child Vulkan owners explicitly before their parents instead of relying on field order.
+ self.swapchain.take();
+ self.device.take();
+ self.surface.take();
+ self.validation.take();
+ self.instance.take();
+ }
+}
+
+impl Drop for VulkanSmokeRenderer {
+ fn drop(&mut self) {
+ self.teardown();
+ }
+}
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 498e260..5dabed6 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -1244,6 +1244,7 @@ const AUDITED_UNSAFE_SOURCE_FILES: &[&str] = &[
"adapters/fparkan-render-vulkan/src/ffi/instance.rs",
"adapters/fparkan-render-vulkan/src/ffi/resources.rs",
"adapters/fparkan-render-vulkan/src/ffi/runtime.rs",
+ "adapters/fparkan-render-vulkan/src/ffi/smoke.rs",
"adapters/fparkan-render-vulkan/src/ffi/surface.rs",
"adapters/fparkan-render-vulkan/src/ffi/validation.rs",
];