#![allow(unsafe_code)] use ash::vk; use fparkan_platform::DepthStencilSupport; use super::{ VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeRendererError, VulkanStaticMesh, VulkanStaticTexture, }; use crate::select_depth_stencil_attachment_format; pub(super) struct VulkanAllocatedBuffer { pub(super) buffer: vk::Buffer, pub(super) memory: vk::DeviceMemory, } pub(super) struct VulkanAllocatedImage { pub(super) image: vk::Image, pub(super) memory: vk::DeviceMemory, pub(super) view: vk::ImageView, pub(super) width: u32, pub(super) height: u32, pub(super) mip_levels: u32, } /// Depth/stencil image and the exact format selected for its render pass. pub(super) struct VulkanDepthAttachment { pub(super) image: VulkanAllocatedImage, pub(super) format: vk::Format, } pub(super) fn create_depth_attachment( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, extent: (u32, u32), request: DepthStencilSupport, ) -> Result { let supported_formats = depth_attachment_formats(instance, device); let format = select_depth_stencil_attachment_format(&supported_formats, request) .map(vk::Format::from_raw) .ok_or(VulkanSmokeRendererError::InvalidStaticMesh { context: "logical device has no selected depth attachment format", })?; let image_info = vk::ImageCreateInfo::default() .image_type(vk::ImageType::TYPE_2D) .format(format) .extent(vk::Extent3D { width: extent.0, height: extent.1, depth: 1, }) .mip_levels(1) .array_layers(1) .samples(vk::SampleCountFlags::TYPE_1) .tiling(vk::ImageTiling::OPTIMAL) .usage(vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT) .sharing_mode(vk::SharingMode::EXCLUSIVE) .initial_layout(vk::ImageLayout::UNDEFINED); // SAFETY: The creation info is stack-owned and uses the live non-zero swapchain extent. let image = unsafe { device.device().create_image(&image_info, None) }.map_err(|result| { VulkanSmokeRendererError::VulkanOperation { context: "vkCreateImage(depth attachment)", result, } })?; // SAFETY: The newly created image belongs to this device. let requirements = unsafe { device.device().get_image_memory_requirements(image) }; let Some(memory_type_index) = find_memory_type( instance, device.physical_device(), requirements.memory_type_bits, vk::MemoryPropertyFlags::DEVICE_LOCAL, ) else { // SAFETY: The unbound image is rolled back on its owning device. unsafe { device.device().destroy_image(image, None) }; return Err(VulkanSmokeRendererError::MissingMemoryType { context: "depth attachment", }); }; let allocation = vk::MemoryAllocateInfo::default() .allocation_size(requirements.size) .memory_type_index(memory_type_index); // SAFETY: Allocation parameters are derived from this image's requirements. let allocation_result = unsafe { device.device().allocate_memory(&allocation, None) }; let memory = match allocation_result { Ok(memory) => memory, Err(result) => { // SAFETY: The unbound image is rolled back on allocation failure. unsafe { device.device().destroy_image(image, None) }; return Err(VulkanSmokeRendererError::VulkanOperation { context: "vkAllocateMemory(depth attachment)", result, }); } }; // SAFETY: The allocation satisfies this image's queried requirements. if let Err(result) = unsafe { device.device().bind_image_memory(image, memory, 0) } { // SAFETY: Both newly created resources are rolled back together. unsafe { device.device().destroy_image(image, None); device.device().free_memory(memory, None); } return Err(VulkanSmokeRendererError::VulkanOperation { context: "vkBindImageMemory(depth attachment)", result, }); } let range = vk::ImageSubresourceRange::default() .aspect_mask(depth_aspect(format)) .base_mip_level(0) .level_count(1) .base_array_layer(0) .layer_count(1); let view_info = vk::ImageViewCreateInfo::default() .image(image) .view_type(vk::ImageViewType::TYPE_2D) .format(format) .subresource_range(range); // SAFETY: The image is bound and the depth/stencil aspect matches its selected format. let view_result = unsafe { device.device().create_image_view(&view_info, None) }; let view = match view_result { Ok(view) => view, Err(result) => { // SAFETY: Both newly created resources are rolled back together. unsafe { device.device().destroy_image(image, None); device.device().free_memory(memory, None); } return Err(VulkanSmokeRendererError::VulkanOperation { context: "vkCreateImageView(depth attachment)", result, }); } }; Ok(VulkanDepthAttachment { image: VulkanAllocatedImage { image, memory, view, width: extent.0, height: extent.1, mip_levels: 1, }, format, }) } pub(super) fn destroy_depth_attachment( device: &VulkanLogicalDeviceProbe, attachment: &VulkanDepthAttachment, ) { destroy_allocated_image(device, &attachment.image); } fn depth_attachment_formats( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, ) -> Vec { [ vk::Format::D16_UNORM, vk::Format::X8_D24_UNORM_PACK32, vk::Format::D32_SFLOAT, vk::Format::D16_UNORM_S8_UINT, vk::Format::D24_UNORM_S8_UINT, vk::Format::D32_SFLOAT_S8_UINT, ] .into_iter() .filter(|format| { // SAFETY: The physical device belongs to this instance and the query returns a value copy. unsafe { instance .instance .get_physical_device_format_properties(device.physical_device(), *format) } .optimal_tiling_features .contains(vk::FormatFeatureFlags::DEPTH_STENCIL_ATTACHMENT) }) .map(vk::Format::as_raw) .collect() } fn depth_aspect(format: vk::Format) -> vk::ImageAspectFlags { match format { vk::Format::D16_UNORM_S8_UINT | vk::Format::D24_UNORM_S8_UINT | vk::Format::D32_SFLOAT_S8_UINT => { vk::ImageAspectFlags::DEPTH | vk::ImageAspectFlags::STENCIL } _ => vk::ImageAspectFlags::DEPTH, } } pub(super) struct VulkanFrameSync { pub(super) image_available: vk::Semaphore, pub(super) fence: vk::Fence, } pub(super) fn create_command_pool( device: &VulkanLogicalDeviceProbe, ) -> Result { let create_info = vk::CommandPoolCreateInfo::default() .queue_family_index(device.report.graphics_queue_family) .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER); // SAFETY: The queue-family index belongs to this live logical device. unsafe { device.device().create_command_pool(&create_info, None) }.map_err(|error| { VulkanSmokeRendererError::VulkanOperation { context: "vkCreateCommandPool", result: error, } }) } pub(super) fn create_static_mesh_vertex_buffer( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, mesh: &VulkanStaticMesh, ) -> Result { let bytes = encode_static_vertices(&mesh.vertices)?; create_host_visible_buffer( instance, device, &bytes, vk::BufferUsageFlags::VERTEX_BUFFER, "static mesh vertex buffer", ) } /// Allocates a vertex buffer for a frame's retained mesh capacity. pub(super) fn create_vertex_buffer( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, vertices: &[super::VulkanStaticVertex], ) -> Result { let bytes = encode_static_vertices(vertices)?; create_host_visible_buffer( instance, device, &bytes, vk::BufferUsageFlags::VERTEX_BUFFER, "dynamic mesh vertex buffer", ) } /// Replaces the bytes of a host-visible static vertex allocation. /// /// The live renderer waits for the device before calling this helper, so a /// single shared allocation can safely carry camera-relative sky and weather /// geometry between frames. pub(super) fn write_static_mesh_vertex_buffer( device: &VulkanLogicalDeviceProbe, buffer: &VulkanAllocatedBuffer, vertices: &[super::VulkanStaticVertex], ) -> Result<(), VulkanSmokeRendererError> { let bytes = encode_static_vertices(vertices)?; write_mapped_buffer( device, buffer, &bytes, "vkMapMemory(static mesh vertex update)", ) } fn encode_static_vertices( vertices: &[super::VulkanStaticVertex], ) -> Result, VulkanSmokeRendererError> { let byte_len = vertices .len() .checked_mul(14) .and_then(|count| count.checked_mul(std::mem::size_of::())) .ok_or(VulkanSmokeRendererError::InvalidStaticMesh { context: "static mesh vertex data size overflows address space", })?; let mut bytes = Vec::new(); bytes .try_reserve_exact(byte_len) .map_err(|_| VulkanSmokeRendererError::InvalidStaticMesh { context: "static mesh vertex data exceeds addressable memory", })?; for vertex in vertices { for value in vertex .position .into_iter() .chain(vertex.color) .chain(vertex.normal) .chain(vertex.uv) .chain(vertex.detail_uv) .chain([vertex.overlay_alpha]) { bytes.extend_from_slice(&value.to_ne_bytes()); } } Ok(bytes) } fn write_mapped_buffer( device: &VulkanLogicalDeviceProbe, buffer: &VulkanAllocatedBuffer, bytes: &[u8], context: &'static str, ) -> Result<(), VulkanSmokeRendererError> { let byte_len = u64::try_from(bytes.len()).map_err(|_| VulkanSmokeRendererError::InvalidStaticMesh { context: "static mesh upload exceeds address space", })?; if byte_len == 0 { return Err(VulkanSmokeRendererError::InvalidStaticMesh { context: "static mesh upload cannot be empty", }); } // SAFETY: The caller validates that the allocation is large enough and // waits for all queue work that could read it before this write. let mapped = unsafe { device .device() .map_memory(buffer.memory, 0, byte_len, vk::MemoryMapFlags::empty()) } .map_err(|result| VulkanSmokeRendererError::VulkanOperation { context, result })?; // SAFETY: `mapped` covers exactly the byte slice copied below. unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), mapped.cast::(), bytes.len()); device.device().unmap_memory(buffer.memory); } Ok(()) } pub(super) fn create_static_mesh_index_buffer( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, mesh: &VulkanStaticMesh, ) -> Result { create_index_buffer(instance, device, &mesh.indices) } /// Allocates an index buffer for a frame's retained mesh capacity. pub(super) fn create_index_buffer( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, indices: &[u32], ) -> Result { let bytes = encode_static_indices(indices)?; create_host_visible_buffer( instance, device, &bytes, vk::BufferUsageFlags::INDEX_BUFFER, "dynamic mesh index buffer", ) } /// Replaces the bytes of a host-visible static index allocation. pub(super) fn write_static_mesh_index_buffer( device: &VulkanLogicalDeviceProbe, buffer: &VulkanAllocatedBuffer, indices: &[u32], ) -> Result<(), VulkanSmokeRendererError> { let bytes = encode_static_indices(indices)?; write_mapped_buffer( device, buffer, &bytes, "vkMapMemory(static mesh index update)", ) } fn encode_static_indices(indices: &[u32]) -> Result, VulkanSmokeRendererError> { let byte_len = indices .len() .checked_mul(std::mem::size_of::()) .ok_or(VulkanSmokeRendererError::InvalidStaticMesh { context: "static mesh index data size overflows address space", })?; let mut bytes = Vec::new(); bytes .try_reserve_exact(byte_len) .map_err(|_| VulkanSmokeRendererError::InvalidStaticMesh { context: "static mesh index data exceeds addressable memory", })?; for &index in indices { bytes.extend_from_slice(&index.to_ne_bytes()); } Ok(bytes) } pub(super) fn create_static_texture_image( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, command_pool: vk::CommandPool, texture: &VulkanStaticTexture, ) -> Result { texture .validate() .map_err(|context| VulkanSmokeRendererError::InvalidStaticTexture { context })?; let (mip_bytes, mip_levels) = build_static_texture_mip_chain(texture); let staging = create_host_visible_buffer( instance, device, &mip_bytes, vk::BufferUsageFlags::TRANSFER_SRC, "static texture staging buffer", )?; let image_info = vk::ImageCreateInfo::default() .image_type(vk::ImageType::TYPE_2D) .format(vk::Format::R8G8B8A8_UNORM) .extent(vk::Extent3D { width: texture.width, height: texture.height, depth: 1, }) .mip_levels(mip_levels) .array_layers(1) .samples(vk::SampleCountFlags::TYPE_1) .tiling(vk::ImageTiling::OPTIMAL) .usage(vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED) .sharing_mode(vk::SharingMode::EXCLUSIVE) .initial_layout(vk::ImageLayout::UNDEFINED); // SAFETY: The create info only contains stack-owned values and a validated non-zero extent. let image = unsafe { device.device().create_image(&image_info, None) }.map_err(|result| { destroy_allocated_buffer(device, &staging); VulkanSmokeRendererError::VulkanOperation { context: "vkCreateImage(static texture)", result, } })?; // SAFETY: The image belongs to this device and is queried immediately after creation. let requirements = unsafe { device.device().get_image_memory_requirements(image) }; let Some(memory_type_index) = find_memory_type( instance, device.physical_device(), requirements.memory_type_bits, vk::MemoryPropertyFlags::DEVICE_LOCAL, ) else { // SAFETY: Both resources were created on this device and are being rolled back once. unsafe { device.device().destroy_image(image, None) }; destroy_allocated_buffer(device, &staging); return Err(VulkanSmokeRendererError::MissingMemoryType { context: "static texture image", }); }; let allocate_info = vk::MemoryAllocateInfo::default() .allocation_size(requirements.size) .memory_type_index(memory_type_index); // SAFETY: The allocation matches the image memory requirements queried above. let memory = { // SAFETY: The allocation matches the image memory requirements queried above. unsafe { device.device().allocate_memory(&allocate_info, None) } } .map_err(|result| { // SAFETY: Both resources were created on this device and are being rolled back once. unsafe { device.device().destroy_image(image, None) }; destroy_allocated_buffer(device, &staging); VulkanSmokeRendererError::VulkanOperation { context: "vkAllocateMemory(static texture)", result, } })?; // SAFETY: The allocation matches this image's queried requirements. if let Err(result) = unsafe { device.device().bind_image_memory(image, memory, 0) } { // SAFETY: Both resources were created on this device and are being rolled back once. unsafe { device.device().destroy_image(image, None); device.device().free_memory(memory, None); }; destroy_allocated_buffer(device, &staging); return Err(VulkanSmokeRendererError::VulkanOperation { context: "vkBindImageMemory(static texture)", result, }); } if let Err(error) = upload_static_texture( device, command_pool, staging.buffer, image, texture.width, texture.height, mip_levels, ) { // SAFETY: Both resources were created on this device and are being rolled back once. unsafe { device.device().destroy_image(image, None); device.device().free_memory(memory, None); }; destroy_allocated_buffer(device, &staging); return Err(error); } destroy_allocated_buffer(device, &staging); let view_info = vk::ImageViewCreateInfo::default() .image(image) .view_type(vk::ImageViewType::TYPE_2D) .format(vk::Format::R8G8B8A8_UNORM) .subresource_range(color_subresource_range(mip_levels)); // SAFETY: The image is live, initialized and has the stated color subresource. let view = { // SAFETY: The image is live, initialized and has the stated color subresource. unsafe { device.device().create_image_view(&view_info, None) } } .map_err(|result| { // SAFETY: Both resources were created on this device and are being rolled back once. unsafe { device.device().destroy_image(image, None); device.device().free_memory(memory, None); }; VulkanSmokeRendererError::VulkanOperation { context: "vkCreateImageView(static texture)", result, } })?; Ok(VulkanAllocatedImage { image, memory, view, width: texture.width, height: texture.height, mip_levels, }) } /// Allocates two aligned camera slots used by the dynamic frame UBO. pub(super) fn create_frame_uniform_buffer( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, ) -> Result<(VulkanAllocatedBuffer, u64), VulkanSmokeRendererError> { const FRAME_SLOTS: u64 = 2; const FRAME_BYTES: u64 = super::VULKAN_FRAME_UNIFORM_BYTES as u64; let limits = // SAFETY: The physical device belongs to this live instance and the query returns a value copy. unsafe { instance.instance.get_physical_device_properties(device.physical_device()) }; let alignment = u64::from(limits.limits.min_uniform_buffer_offset_alignment).max(1); let stride = FRAME_BYTES.checked_add(alignment - 1).ok_or( VulkanSmokeRendererError::InvalidStaticMesh { context: "frame uniform stride overflow", }, )? / alignment * alignment; let size = stride .checked_mul(FRAME_SLOTS) .ok_or(VulkanSmokeRendererError::InvalidStaticMesh { context: "frame uniform buffer size overflow", })?; let bytes = vec![ 0_u8; usize::try_from(size).map_err(|_| { VulkanSmokeRendererError::InvalidStaticMesh { context: "frame uniform buffer size exceeds address space", } })? ]; let buffer = create_host_visible_buffer( instance, device, &bytes, vk::BufferUsageFlags::UNIFORM_BUFFER, "frame uniform buffer", )?; Ok((buffer, stride)) } /// Allocates one aligned storage-buffer record for every material descriptor. /// /// The descriptor uses a dynamic storage-buffer offset. Keeping the stride /// aligned to the physical device limit lets the renderer select a material /// record with the same descriptor set for every draw without baking material /// indices into the pipeline layout. pub(super) fn create_material_specular_buffer( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, material_count: usize, ) -> Result<(VulkanAllocatedBuffer, u64), VulkanSmokeRendererError> { if material_count == 0 { return Err(VulkanSmokeRendererError::InvariantViolation { context: "material specular buffer has no material records", }); } let limits = // SAFETY: The physical device belongs to this live instance and the query returns a value copy. unsafe { instance.instance.get_physical_device_properties(device.physical_device()) }; let alignment = limits.limits.min_storage_buffer_offset_alignment.max(1); let record_bytes = super::VULKAN_MATERIAL_SPECULAR_RECORD_BYTES as u64; let stride = record_bytes.checked_add(alignment - 1).ok_or( VulkanSmokeRendererError::InvalidStaticMesh { context: "material specular stride overflow", }, )? / alignment * alignment; let count = u64::try_from(material_count).map_err(|_| VulkanSmokeRendererError::InvalidStaticMesh { context: "material specular count exceeds address space", })?; let size = stride .checked_mul(count) .ok_or(VulkanSmokeRendererError::InvalidStaticMesh { context: "material specular buffer size overflow", })?; let bytes = vec![ 0_u8; usize::try_from(size).map_err(|_| VulkanSmokeRendererError::InvalidStaticMesh { context: "material specular buffer size exceeds address space", })? ]; let buffer = create_host_visible_buffer( instance, device, &bytes, vk::BufferUsageFlags::STORAGE_BUFFER, "material specular buffer", )?; Ok((buffer, stride)) } /// Replaces all material specular records in one host-coherent upload. pub(super) fn write_material_specular_buffer( device: &VulkanLogicalDeviceProbe, buffer: &VulkanAllocatedBuffer, stride: u64, records: &[[[f32; 4]; 2]], ) -> Result<(), VulkanSmokeRendererError> { if records.is_empty() { return Err(VulkanSmokeRendererError::InvariantViolation { context: "material specular upload has no records", }); } let record_bytes = super::VULKAN_MATERIAL_SPECULAR_RECORD_BYTES; let total_bytes = stride .checked_mul(u64::try_from(records.len()).unwrap_or(u64::MAX)) .ok_or(VulkanSmokeRendererError::InvariantViolation { context: "material specular upload size", })?; let total_bytes_usize = usize::try_from(total_bytes).map_err(|_| VulkanSmokeRendererError::InvariantViolation { context: "material specular upload size exceeds address space", })?; let mut bytes = vec![0_u8; total_bytes_usize]; for (record_index, record) in records.iter().enumerate() { let record_offset = usize::try_from( stride .checked_mul(u64::try_from(record_index).unwrap_or(u64::MAX)) .ok_or(VulkanSmokeRendererError::InvariantViolation { context: "material specular upload offset", })?, ) .map_err(|_| VulkanSmokeRendererError::InvariantViolation { context: "material specular upload offset exceeds address space", })?; let mut offset = record_offset; for vector in record { for value in vector { bytes[offset..offset + std::mem::size_of::()] .copy_from_slice(&value.to_ne_bytes()); offset += std::mem::size_of::(); } } debug_assert_eq!(offset - record_offset, record_bytes); } // SAFETY: The storage buffer is host-visible and the caller has waited for // all prior uses before replacing its records. let mapped = unsafe { device .device() .map_memory(buffer.memory, 0, total_bytes, vk::MemoryMapFlags::empty()) } .map_err(|result| VulkanSmokeRendererError::VulkanOperation { context: "vkMapMemory(material specular buffer)", result, })?; // SAFETY: The mapped range is the complete upload and the destination is // valid for the exact byte count copied below. unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), mapped.cast::(), bytes.len()); device.device().unmap_memory(buffer.memory); } Ok(()) } /// Writes one complete frame block into the slot selected by the /// fence-safe frame index. pub(super) fn write_frame_uniform( device: &VulkanLogicalDeviceProbe, buffer: &VulkanAllocatedBuffer, stride: u64, frame_index: usize, bytes: &[u8; super::VULKAN_FRAME_UNIFORM_BYTES], ) -> Result<(), VulkanSmokeRendererError> { let slot = u64::try_from(frame_index).map_err(|_| VulkanSmokeRendererError::InvariantViolation { context: "frame uniform slot index", })?; let offset = stride .checked_mul(slot) .ok_or(VulkanSmokeRendererError::InvariantViolation { context: "frame uniform slot offset", })?; // SAFETY: The offset is one of the two allocated aligned slots and the // mapped range is exactly one complete frame block. let mapped = unsafe { device.device().map_memory( buffer.memory, offset, super::VULKAN_FRAME_UNIFORM_BYTES as u64, vk::MemoryMapFlags::empty(), ) } .map_err(|result| VulkanSmokeRendererError::VulkanOperation { context: "vkMapMemory(frame uniform buffer)", result, })?; // SAFETY: The destination is the mapped frame slot and the byte slice lives // for this copy. unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), mapped.cast::(), bytes.len()); device.device().unmap_memory(buffer.memory); } Ok(()) } fn upload_static_texture( device: &VulkanLogicalDeviceProbe, command_pool: vk::CommandPool, staging_buffer: vk::Buffer, image: vk::Image, width: u32, height: u32, mip_levels: u32, ) -> Result<(), VulkanSmokeRendererError> { let allocate_info = vk::CommandBufferAllocateInfo::default() .command_pool(command_pool) .level(vk::CommandBufferLevel::PRIMARY) .command_buffer_count(1); // SAFETY: The command pool is live and owned by the current logical device. let command_buffer = unsafe { device.device().allocate_command_buffers(&allocate_info) } .map_err(|result| VulkanSmokeRendererError::VulkanOperation { context: "vkAllocateCommandBuffers(texture upload)", result, })?[0]; let begin = vk::CommandBufferBeginInfo::default().flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); // SAFETY: The command buffer is freshly allocated from the current pool. if let Err(result) = unsafe { device.device().begin_command_buffer(command_buffer, &begin) } { // SAFETY: The command buffer belongs to this pool and is released on failure. unsafe { device .device() .free_command_buffers(command_pool, &[command_buffer]); }; return Err(VulkanSmokeRendererError::VulkanOperation { context: "vkBeginCommandBuffer(texture upload)", result, }); } let range = color_subresource_range(mip_levels); let to_transfer = vk::ImageMemoryBarrier::default() .old_layout(vk::ImageLayout::UNDEFINED) .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) .src_access_mask(vk::AccessFlags::empty()) .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE) .image(image) .subresource_range(range); let mut regions = Vec::with_capacity(usize::try_from(mip_levels).unwrap_or(0)); let mut buffer_offset = 0_u64; let mut mip_width = width; let mut mip_height = height; for mip_level in 0..mip_levels { let mip_bytes = u64::from(mip_width) .saturating_mul(u64::from(mip_height)) .saturating_mul(4); regions.push( vk::BufferImageCopy::default() .buffer_offset(buffer_offset) .image_subresource( vk::ImageSubresourceLayers::default() .aspect_mask(vk::ImageAspectFlags::COLOR) .mip_level(mip_level) .base_array_layer(0) .layer_count(1), ) .image_extent(vk::Extent3D { width: mip_width, height: mip_height, depth: 1, }), ); buffer_offset = buffer_offset.saturating_add(mip_bytes); mip_width = (mip_width / 2).max(1); mip_height = (mip_height / 2).max(1); } let to_sampled = vk::ImageMemoryBarrier::default() .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) .src_access_mask(vk::AccessFlags::TRANSFER_WRITE) .dst_access_mask(vk::AccessFlags::SHADER_READ) .image(image) .subresource_range(range); // SAFETY: The commands operate on live, exclusively owned resources and the stated color subresource. unsafe { device.device().cmd_pipeline_barrier( command_buffer, vk::PipelineStageFlags::TOP_OF_PIPE, vk::PipelineStageFlags::TRANSFER, vk::DependencyFlags::empty(), &[], &[], &[to_transfer], ); device.device().cmd_copy_buffer_to_image( command_buffer, staging_buffer, image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, ®ions, ); device.device().cmd_pipeline_barrier( command_buffer, vk::PipelineStageFlags::TRANSFER, vk::PipelineStageFlags::FRAGMENT_SHADER, vk::DependencyFlags::empty(), &[], &[], &[to_sampled], ); } // SAFETY: Command recording is complete on the current command buffer. if let Err(result) = unsafe { device.device().end_command_buffer(command_buffer) } { // SAFETY: The command buffer belongs to this pool and is released on failure. unsafe { device .device() .free_command_buffers(command_pool, &[command_buffer]); }; return Err(VulkanSmokeRendererError::VulkanOperation { context: "vkEndCommandBuffer(texture upload)", result, }); } let command_buffers = [command_buffer]; let submit = [vk::SubmitInfo::default().command_buffers(&command_buffers)]; // SAFETY: The graphics queue and submitted command buffer are live for the duration of the wait. let submit_result = unsafe { device .device() .queue_submit(device.graphics_queue(), &submit, vk::Fence::null()) }; let result = submit_result.and_then(|()| { // SAFETY: The graphics queue remains live until the synchronous idle wait completes. unsafe { device.device().queue_wait_idle(device.graphics_queue()) } }); // SAFETY: Submission completed or failed synchronously and the command buffer is no longer needed. unsafe { device .device() .free_command_buffers(command_pool, &[command_buffer]); }; result.map_err(|result| VulkanSmokeRendererError::VulkanOperation { context: "vkQueueSubmit/WaitIdle(texture upload)", result, }) } /// Replaces the contents of an already sampled texture without changing its /// image view or descriptor set. The caller must wait for the renderer's /// submitted work before invoking this function. pub(super) fn update_static_texture_image( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, command_pool: vk::CommandPool, image: &VulkanAllocatedImage, texture: &VulkanStaticTexture, ) -> Result<(), VulkanSmokeRendererError> { texture .validate() .map_err(|context| VulkanSmokeRendererError::InvalidStaticTexture { context })?; let (mip_bytes, mip_levels) = build_static_texture_mip_chain(texture); if texture.width != image.width || texture.height != image.height || mip_levels != image.mip_levels { return Err(VulkanSmokeRendererError::InvalidStaticTexture { context: "dynamic texture extent or mip count differs from allocation", }); } let staging = create_host_visible_buffer( instance, device, &mip_bytes, vk::BufferUsageFlags::TRANSFER_SRC, "dynamic texture staging buffer", )?; let allocate_info = vk::CommandBufferAllocateInfo::default() .command_pool(command_pool) .level(vk::CommandBufferLevel::PRIMARY) .command_buffer_count(1); // SAFETY: The command pool is live and owned by the current logical device. let command_buffer = unsafe { device.device().allocate_command_buffers(&allocate_info) } .map_err(|result| { destroy_allocated_buffer(device, &staging); VulkanSmokeRendererError::VulkanOperation { context: "vkAllocateCommandBuffers(dynamic texture upload)", result, } })?[0]; let begin = vk::CommandBufferBeginInfo::default().flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); // SAFETY: The command buffer is freshly allocated from the current pool. if let Err(result) = unsafe { device.device().begin_command_buffer(command_buffer, &begin) } { // SAFETY: The command buffer belongs to this pool and is released on failure. unsafe { device .device() .free_command_buffers(command_pool, &[command_buffer]); }; destroy_allocated_buffer(device, &staging); return Err(VulkanSmokeRendererError::VulkanOperation { context: "vkBeginCommandBuffer(dynamic texture upload)", result, }); } let range = color_subresource_range(mip_levels); let to_transfer = vk::ImageMemoryBarrier::default() .old_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) .src_access_mask(vk::AccessFlags::SHADER_READ) .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE) .image(image.image) .subresource_range(range); let mut regions = Vec::with_capacity(usize::try_from(mip_levels).unwrap_or(0)); let mut buffer_offset = 0_u64; let mut mip_width = texture.width; let mut mip_height = texture.height; for mip_level in 0..mip_levels { let mip_bytes = u64::from(mip_width) .saturating_mul(u64::from(mip_height)) .saturating_mul(4); regions.push( vk::BufferImageCopy::default() .buffer_offset(buffer_offset) .image_subresource( vk::ImageSubresourceLayers::default() .aspect_mask(vk::ImageAspectFlags::COLOR) .mip_level(mip_level) .base_array_layer(0) .layer_count(1), ) .image_extent(vk::Extent3D { width: mip_width, height: mip_height, depth: 1, }), ); buffer_offset = buffer_offset.saturating_add(mip_bytes); mip_width = (mip_width / 2).max(1); mip_height = (mip_height / 2).max(1); } let to_sampled = vk::ImageMemoryBarrier::default() .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) .src_access_mask(vk::AccessFlags::TRANSFER_WRITE) .dst_access_mask(vk::AccessFlags::SHADER_READ) .image(image.image) .subresource_range(range); // SAFETY: The caller has made all prior shader reads idle, and the image // remains exclusively owned by this graphics queue. unsafe { device.device().cmd_pipeline_barrier( command_buffer, vk::PipelineStageFlags::FRAGMENT_SHADER, vk::PipelineStageFlags::TRANSFER, vk::DependencyFlags::empty(), &[], &[], &[to_transfer], ); device.device().cmd_copy_buffer_to_image( command_buffer, staging.buffer, image.image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, ®ions, ); device.device().cmd_pipeline_barrier( command_buffer, vk::PipelineStageFlags::TRANSFER, vk::PipelineStageFlags::FRAGMENT_SHADER, vk::DependencyFlags::empty(), &[], &[], &[to_sampled], ); } // SAFETY: Command recording is complete on the current command buffer. if let Err(result) = unsafe { device.device().end_command_buffer(command_buffer) } { // SAFETY: The command buffer belongs to this pool and is released on failure. unsafe { device .device() .free_command_buffers(command_pool, &[command_buffer]); }; destroy_allocated_buffer(device, &staging); return Err(VulkanSmokeRendererError::VulkanOperation { context: "vkEndCommandBuffer(dynamic texture upload)", result, }); } let command_buffers = [command_buffer]; let submit = [vk::SubmitInfo::default().command_buffers(&command_buffers)]; // SAFETY: The graphics queue and submitted command buffer remain live // until this synchronous wait completes. let submit_result = unsafe { device .device() .queue_submit(device.graphics_queue(), &submit, vk::Fence::null()) }; let result = submit_result .and_then(|()| unsafe { device.device().queue_wait_idle(device.graphics_queue()) }); // SAFETY: Submission completed or failed synchronously. unsafe { device .device() .free_command_buffers(command_pool, &[command_buffer]); } destroy_allocated_buffer(device, &staging); result.map_err(|result| VulkanSmokeRendererError::VulkanOperation { context: "vkQueueSubmit/WaitIdle(dynamic texture upload)", result, }) } /// Builds an RGBA8 mip chain on the CPU so texture upload does not depend on /// optional linear-blit support for the device's UNORM format. fn build_static_texture_mip_chain(texture: &VulkanStaticTexture) -> (Vec, u32) { if !texture.mip_levels.is_empty() { let mut bytes = Vec::new(); for mip in &texture.mip_levels { bytes.extend_from_slice(&mip.rgba8); } return ( bytes, u32::try_from(texture.mip_levels.len()).unwrap_or(u32::MAX), ); } let mut bytes = texture.rgba8.clone(); let mut level = texture.rgba8.clone(); let mut width = texture.width; let mut height = texture.height; let mut mip_levels = 1_u32; while width > 1 || height > 1 { let next_width = (width / 2).max(1); let next_height = (height / 2).max(1); let mut next = vec![ 0_u8; usize::try_from(next_width) .unwrap_or(0) .saturating_mul(usize::try_from(next_height).unwrap_or(0)) .saturating_mul(4) ]; for y in 0..next_height { for x in 0..next_width { let dst = (usize::try_from(y).unwrap_or(0) * usize::try_from(next_width).unwrap_or(0) + usize::try_from(x).unwrap_or(0)) * 4; let x0 = x.saturating_mul(2).min(width.saturating_sub(1)); let y0 = y.saturating_mul(2).min(height.saturating_sub(1)); let x1 = x0.saturating_add(1).min(width.saturating_sub(1)); let y1 = y0.saturating_add(1).min(height.saturating_sub(1)); let source_width = usize::try_from(width).unwrap_or(0); let samples = [ (usize::try_from(y0).unwrap_or(0) * source_width + usize::try_from(x0).unwrap_or(0)) * 4, (usize::try_from(y0).unwrap_or(0) * source_width + usize::try_from(x1).unwrap_or(0)) * 4, (usize::try_from(y1).unwrap_or(0) * source_width + usize::try_from(x0).unwrap_or(0)) * 4, (usize::try_from(y1).unwrap_or(0) * source_width + usize::try_from(x1).unwrap_or(0)) * 4, ]; for channel in 0..4 { let sum: u16 = samples .iter() .map(|&offset| u16::from(level[offset + channel])) .sum(); next[dst + channel] = u8::try_from((sum + 2) / 4).unwrap_or(u8::MAX); } } } bytes.extend_from_slice(&next); level = next; width = next_width; height = next_height; mip_levels = mip_levels.saturating_add(1); } (bytes, mip_levels) } pub(super) fn destroy_allocated_image( device: &VulkanLogicalDeviceProbe, image: &VulkanAllocatedImage, ) { // SAFETY: The image, view and allocation belong to this device and are destroyed once after idle. unsafe { device.device().destroy_image_view(image.view, None); device.device().destroy_image(image.image, None); device.device().free_memory(image.memory, None); }; } fn create_host_visible_buffer( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, bytes: &[u8], usage: vk::BufferUsageFlags, context: &'static str, ) -> Result { let create_info = vk::BufferCreateInfo::default() .size(bytes.len().try_into().unwrap_or(u64::MAX)) .usage(usage) .sharing_mode(vk::SharingMode::EXCLUSIVE); // SAFETY: The create info is stack-owned and references no external memory. let buffer = unsafe { device.device().create_buffer(&create_info, None) }.map_err(|error| { VulkanSmokeRendererError::VulkanOperation { context, result: error, } })?; // SAFETY: The buffer belongs to this device and is queried immediately after creation. let requirements = unsafe { device.device().get_buffer_memory_requirements(buffer) }; let Some(memory_type_index) = find_memory_type( instance, device.physical_device(), requirements.memory_type_bits, vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, ) else { // SAFETY: The buffer was created above on this logical device and is destroyed on setup failure. unsafe { device.device().destroy_buffer(buffer, None) }; return Err(VulkanSmokeRendererError::MissingMemoryType { context }); }; let allocate_info = vk::MemoryAllocateInfo::default() .allocation_size(requirements.size) .memory_type_index(memory_type_index); let memory = // SAFETY: The allocation request matches the queried memory requirements for this buffer. unsafe { device.device().allocate_memory(&allocate_info, None) }.map_err(|error| { // SAFETY: The buffer was created above on this logical device and is destroyed on setup failure. unsafe { device.device().destroy_buffer(buffer, None) }; VulkanSmokeRendererError::VulkanOperation { context, result: error, } })?; // SAFETY: The allocation satisfies the queried buffer memory requirements for this device. unsafe { device.device().bind_buffer_memory(buffer, memory, 0) }.map_err(|error| { // SAFETY: The buffer and allocation were created above on this logical device and are destroyed on setup failure. unsafe { device.device().destroy_buffer(buffer, None); device.device().free_memory(memory, None); } VulkanSmokeRendererError::VulkanOperation { context, result: error, } })?; // SAFETY: The mapping range is within the host-visible allocation bound to the buffer. let mapped = unsafe { device .device() .map_memory(memory, 0, requirements.size, vk::MemoryMapFlags::empty()) } .map_err(|error| { // SAFETY: The buffer and allocation were created above on this logical device and are destroyed on setup failure. unsafe { device.device().destroy_buffer(buffer, None); device.device().free_memory(memory, None); } VulkanSmokeRendererError::VulkanOperation { context, result: error, } })?; // SAFETY: The destination points to the mapped allocation and the source slice lives for the copy. unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), mapped.cast::(), bytes.len()); device.device().unmap_memory(memory); } Ok(VulkanAllocatedBuffer { buffer, memory }) } fn find_memory_type( instance: &VulkanInstanceProbe, physical_device: vk::PhysicalDevice, type_bits: u32, required: vk::MemoryPropertyFlags, ) -> Option { let properties = // SAFETY: The physical device was selected from this live instance and queried by value. unsafe { instance .instance .get_physical_device_memory_properties(physical_device) }; let count = usize::try_from(properties.memory_type_count).unwrap_or(0); properties.memory_types[..count] .iter() .enumerate() .find_map(|(index, memory_type)| { let index_u32 = u32::try_from(index).ok()?; let supported = (type_bits & (1_u32 << index_u32)) != 0; (supported && memory_type.property_flags.contains(required)).then_some(index_u32) }) } pub(super) fn create_frame_sync( device: &VulkanLogicalDeviceProbe, ) -> Result, VulkanSmokeRendererError> { let semaphore_info = vk::SemaphoreCreateInfo::default(); let fence_info = vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED); let mut sync = Vec::with_capacity(2); for _ in 0..2 { // SAFETY: The sync objects belong to this live logical device and are destroyed at teardown. let image_available = match unsafe { device.device().create_semaphore(&semaphore_info, None) } { Ok(image_available) => image_available, Err(error) => { destroy_frame_sync_objects(device, &sync); return Err(VulkanSmokeRendererError::VulkanOperation { context: "vkCreateSemaphore(image_available)", 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) => { destroy_frame_sync_objects(device, &sync); // SAFETY: The semaphore was created above on this logical device and is destroyed on setup failure. unsafe { device.device().destroy_semaphore(image_available, None) }; return Err(VulkanSmokeRendererError::VulkanOperation { context: "vkCreateFence", result: error, }); } }; sync.push(VulkanFrameSync { image_available, fence, }); } Ok(sync) } fn destroy_frame_sync_objects(device: &VulkanLogicalDeviceProbe, sync: &[VulkanFrameSync]) { for frame_sync in sync { // SAFETY: These sync objects belong to this live logical device and are destroyed once during teardown. unsafe { device .device() .destroy_semaphore(frame_sync.image_available, None); device.device().destroy_fence(frame_sync.fence, None); } } } pub(super) fn destroy_allocated_buffer( device: &VulkanLogicalDeviceProbe, buffer: &VulkanAllocatedBuffer, ) { // SAFETY: The buffer and allocation belong to this live logical device and are destroyed once during teardown. unsafe { device.device().destroy_buffer(buffer.buffer, None); device.device().free_memory(buffer.memory, None); } } /// Allocates a host-coherent transfer destination for a swapchain image copy. pub(super) fn create_readback_buffer( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, byte_len: usize, ) -> Result { create_host_visible_buffer( instance, device, &vec![0; byte_len], vk::BufferUsageFlags::TRANSFER_DST, "vkCreateBuffer(readback)", ) } /// Copies a completed host-coherent readback allocation into CPU-owned bytes. pub(super) fn readback_buffer_bytes( device: &VulkanLogicalDeviceProbe, buffer: &VulkanAllocatedBuffer, byte_len: usize, ) -> Result, VulkanSmokeRendererError> { // SAFETY: The caller waits for device idle before mapping this host-visible allocation. let mapped = unsafe { device.device().map_memory( buffer.memory, 0, u64::try_from(byte_len).unwrap_or(u64::MAX), vk::MemoryMapFlags::empty(), ) } .map_err(|result| VulkanSmokeRendererError::VulkanOperation { context: "vkMapMemory(readback)", result, })?; let mut bytes = vec![0; byte_len]; // SAFETY: Both ranges contain exactly byte_len initialized bytes and do not overlap. unsafe { std::ptr::copy_nonoverlapping(mapped.cast::(), bytes.as_mut_ptr(), byte_len); device.device().unmap_memory(buffer.memory); } Ok(bytes) } pub(super) fn color_subresource_range(level_count: u32) -> vk::ImageSubresourceRange { vk::ImageSubresourceRange::default() .aspect_mask(vk::ImageAspectFlags::COLOR) .base_mip_level(0) .level_count(level_count) .base_array_layer(0) .layer_count(1) }