diff options
Diffstat (limited to 'adapters/fparkan-render-vulkan/src/ffi/smoke.rs')
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke.rs | 612 |
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(); + } +} |
