aboutsummaryrefslogtreecommitdiff
path: root/adapters/fparkan-render-vulkan/src/ffi/resources.rs
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-07-18 06:29:31 +0300
committerValentin Popov <valentin@popov.link>2026-07-18 06:29:31 +0300
commit6e6496eebf4057ceb4a09869b0df21b1f0260e81 (patch)
treed6e019a6f653c07b2b75579bc7c25e3bf45cfc90 /adapters/fparkan-render-vulkan/src/ffi/resources.rs
parent283b48e8c512dd251d4617cc988985e3be2a0c5e (diff)
downloadfparkan-6e6496eebf4057ceb4a09869b0df21b1f0260e81.tar.xz
fparkan-6e6496eebf4057ceb4a09869b0df21b1f0260e81.zip
feat(render): upload original TEXM to Vulkan
Diffstat (limited to 'adapters/fparkan-render-vulkan/src/ffi/resources.rs')
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/resources.rs273
1 files changed, 273 insertions, 0 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi/resources.rs b/adapters/fparkan-render-vulkan/src/ffi/resources.rs
index e5d725b..f3726f5 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/resources.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/resources.rs
@@ -4,6 +4,7 @@ use ash::vk;
use super::{
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeRendererError, VulkanStaticMesh,
+ VulkanStaticTexture,
};
pub(super) struct VulkanAllocatedBuffer {
@@ -11,6 +12,12 @@ pub(super) struct VulkanAllocatedBuffer {
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) struct VulkanFrameSync {
pub(super) image_available: vk::Semaphore,
pub(super) render_finished: vk::Semaphore,
@@ -70,6 +77,272 @@ pub(super) fn create_static_mesh_index_buffer(
)
}
+#[allow(clippy::too_many_lines)]
+pub(super) fn create_static_texture_image(
+ instance: &VulkanInstanceProbe,
+ device: &VulkanLogicalDeviceProbe,
+ command_pool: vk::CommandPool,
+ texture: &VulkanStaticTexture,
+) -> Result<VulkanAllocatedImage, VulkanSmokeRendererError> {
+ let staging = create_host_visible_buffer(
+ instance,
+ device,
+ &texture.rgba8,
+ 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(1)
+ .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,
+ ) {
+ // 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());
+ // 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,
+ })
+}
+
+#[allow(clippy::too_many_lines)]
+fn upload_static_texture(
+ device: &VulkanLogicalDeviceProbe,
+ command_pool: vk::CommandPool,
+ staging_buffer: vk::Buffer,
+ image: vk::Image,
+ width: u32,
+ height: 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();
+ 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 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 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,
+ &[region],
+ );
+ 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,
+ })
+}
+
+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,