From a8faba8aad441e87f8a8b94c148c01f9c075d621 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sun, 11 Oct 2026 13:12:50 +0400 Subject: feat: complete map viewer scene and static CTL pose preview Complete the interactive mission viewer with environment rendering, audio events, dynamic shadows, free-flight camera controls, and per-component CTL pose sampling. --- .../fparkan-render-vulkan/src/ffi/resources.rs | 694 +++++++++++++++++++-- 1 file changed, 637 insertions(+), 57 deletions(-) (limited to 'adapters/fparkan-render-vulkan/src/ffi/resources.rs') diff --git a/adapters/fparkan-render-vulkan/src/ffi/resources.rs b/adapters/fparkan-render-vulkan/src/ffi/resources.rs index f1fd579..b841588 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/resources.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/resources.rs @@ -18,6 +18,9 @@ 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. @@ -134,6 +137,9 @@ pub(super) fn create_depth_attachment( image, memory, view, + width: extent.0, + height: extent.1, + mip_levels: 1, }, format, }) @@ -186,7 +192,6 @@ fn depth_aspect(format: vk::Format) -> vk::ImageAspectFlags { pub(super) struct VulkanFrameSync { pub(super) image_available: vk::Semaphore, - pub(super) render_finished: vk::Semaphore, pub(super) fence: vk::Fence, } @@ -210,24 +215,112 @@ pub(super) fn create_static_mesh_vertex_buffer( device: &VulkanLogicalDeviceProbe, mesh: &VulkanStaticMesh, ) -> Result { - let mut bytes = Vec::with_capacity(mesh.vertices.len() * 8 * std::mem::size_of::()); - for vertex in &mesh.vertices { + 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()); } } - create_host_visible_buffer( - instance, - device, - &bytes, - vk::BufferUsageFlags::VERTEX_BUFFER, - "static mesh vertex buffer", - ) + 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( @@ -235,29 +328,73 @@ pub(super) fn create_static_mesh_index_buffer( device: &VulkanLogicalDeviceProbe, mesh: &VulkanStaticMesh, ) -> Result { - let mut bytes = Vec::with_capacity(mesh.indices.len() * std::mem::size_of::()); - for &index in &mesh.indices { - bytes.extend_from_slice(&index.to_ne_bytes()); - } + 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, - "static mesh 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, - &texture.rgba8, + &mip_bytes, vk::BufferUsageFlags::TRANSFER_SRC, "static texture staging buffer", )?; @@ -269,7 +406,7 @@ pub(super) fn create_static_texture_image( height: texture.height, depth: 1, }) - .mip_levels(1) + .mip_levels(mip_levels) .array_layers(1) .samples(vk::SampleCountFlags::TYPE_1) .tiling(vk::ImageTiling::OPTIMAL) @@ -336,6 +473,7 @@ pub(super) fn create_static_texture_image( image, texture.width, texture.height, + mip_levels, ) { // SAFETY: Both resources were created on this device and are being rolled back once. unsafe { @@ -350,7 +488,7 @@ pub(super) fn create_static_texture_image( .image(image) .view_type(vk::ImageViewType::TYPE_2D) .format(vk::Format::R8G8B8A8_UNORM) - .subresource_range(color_subresource_range()); + .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. @@ -371,9 +509,211 @@ pub(super) fn create_static_texture_image( 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, @@ -381,6 +721,7 @@ fn upload_static_texture( image: vk::Image, width: u32, height: u32, + mip_levels: u32, ) -> Result<(), VulkanSmokeRendererError> { let allocate_info = vk::CommandBufferAllocateInfo::default() .command_pool(command_pool) @@ -407,7 +748,7 @@ fn upload_static_texture( result, }); } - let range = color_subresource_range(); + 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) @@ -415,19 +756,34 @@ fn upload_static_texture( .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE) .image(image) .subresource_range(range); - let region = vk::BufferImageCopy::default() - .image_subresource( - vk::ImageSubresourceLayers::default() - .aspect_mask(vk::ImageAspectFlags::COLOR) - .mip_level(0) - .base_array_layer(0) - .layer_count(1), - ) - .image_extent(vk::Extent3D { - width, - height, - depth: 1, - }); + 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) @@ -451,7 +807,7 @@ fn upload_static_texture( staging_buffer, image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, - &[region], + ®ions, ); device.device().cmd_pipeline_barrier( command_buffer, @@ -500,6 +856,246 @@ fn upload_static_texture( }) } +/// 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, @@ -624,36 +1220,24 @@ pub(super) fn create_frame_sync( 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 = unsafe { device.device().create_semaphore(&semaphore_info, None) } - .map_err(|error| VulkanSmokeRendererError::VulkanOperation { - context: "vkCreateSemaphore(image_available)", - result: error, - })?; - let render_finished = { - // 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(render_finished) => render_finished, + Ok(image_available) => image_available, 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: "vkCreateSemaphore(render_finished)", + 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: These semaphores were created above on this logical device and are destroyed on setup failure. - unsafe { - device.device().destroy_semaphore(image_available, None); - device.device().destroy_semaphore(render_finished, None); - } + // 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, @@ -662,7 +1246,6 @@ pub(super) fn create_frame_sync( }; sync.push(VulkanFrameSync { image_available, - render_finished, fence, }); } @@ -676,9 +1259,6 @@ fn destroy_frame_sync_objects(device: &VulkanLogicalDeviceProbe, sync: &[VulkanF device .device() .destroy_semaphore(frame_sync.image_available, None); - device - .device() - .destroy_semaphore(frame_sync.render_finished, None); device.device().destroy_fence(frame_sync.fence, None); } } @@ -738,11 +1318,11 @@ pub(super) fn readback_buffer_bytes( Ok(bytes) } -pub(super) fn color_subresource_range() -> vk::ImageSubresourceRange { +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(1) + .level_count(level_count) .base_array_layer(0) .layer_count(1) } -- cgit v1.2.3