aboutsummaryrefslogtreecommitdiff
path: root/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
diff options
context:
space:
mode:
Diffstat (limited to 'adapters/fparkan-render-vulkan/src/ffi/smoke.rs')
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke.rs612
1 files changed, 612 insertions, 0 deletions
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();
+ }
+}