2026-06-25 06:33:56 +04:00
|
|
|
#![allow(unsafe_code)]
|
|
|
|
|
|
|
|
|
|
use ash::vk;
|
|
|
|
|
|
|
|
|
|
use super::{
|
|
|
|
|
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeRendererError, VulkanSwapchainProbe,
|
|
|
|
|
TRIANGLE_FRAGMENT_SHADER_WORDS, TRIANGLE_VERTEX_SHADER_WORDS,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
pub(super) struct VulkanAllocatedBuffer {
|
|
|
|
|
pub(super) buffer: vk::Buffer,
|
|
|
|
|
pub(super) memory: vk::DeviceMemory,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) struct VulkanSwapchainResources {
|
|
|
|
|
pub(super) image_views: Vec<vk::ImageView>,
|
|
|
|
|
pub(super) render_pass: vk::RenderPass,
|
|
|
|
|
pub(super) pipeline_layout: vk::PipelineLayout,
|
|
|
|
|
pub(super) pipeline: vk::Pipeline,
|
|
|
|
|
pub(super) framebuffers: Vec<vk::Framebuffer>,
|
|
|
|
|
pub(super) command_buffers: Vec<vk::CommandBuffer>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct PartialSwapchainResources {
|
|
|
|
|
image_views: Vec<vk::ImageView>,
|
|
|
|
|
render_pass: Option<vk::RenderPass>,
|
|
|
|
|
pipeline_layout: Option<vk::PipelineLayout>,
|
|
|
|
|
pipeline: Option<vk::Pipeline>,
|
|
|
|
|
framebuffers: Vec<vk::Framebuffer>,
|
|
|
|
|
command_buffers: Vec<vk::CommandBuffer>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) struct VulkanFrameSync {
|
|
|
|
|
pub(super) image_available: vk::Semaphore,
|
|
|
|
|
pub(super) render_finished: vk::Semaphore,
|
|
|
|
|
pub(super) fence: vk::Fence,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn create_command_pool(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
) -> Result<vk::CommandPool, VulkanSmokeRendererError> {
|
|
|
|
|
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_triangle_vertex_buffer(
|
|
|
|
|
instance: &VulkanInstanceProbe,
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
|
|
|
|
let vertices: [[f32; 5]; 3] = [
|
|
|
|
|
[0.0, -0.55, 1.0, 0.2, 0.2],
|
|
|
|
|
[0.55, 0.55, 0.2, 1.0, 0.2],
|
|
|
|
|
[-0.55, 0.55, 0.2, 0.4, 1.0],
|
|
|
|
|
];
|
|
|
|
|
let mut bytes = Vec::with_capacity(vertices.len() * 5 * std::mem::size_of::<f32>());
|
|
|
|
|
for vertex in vertices {
|
|
|
|
|
for value in vertex {
|
|
|
|
|
bytes.extend_from_slice(&value.to_ne_bytes());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
create_host_visible_buffer(
|
|
|
|
|
instance,
|
|
|
|
|
device,
|
|
|
|
|
&bytes,
|
|
|
|
|
vk::BufferUsageFlags::VERTEX_BUFFER,
|
|
|
|
|
"triangle vertex buffer",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn create_triangle_index_buffer(
|
|
|
|
|
instance: &VulkanInstanceProbe,
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
|
|
|
|
let indices = [0_u16, 1_u16, 2_u16];
|
|
|
|
|
let mut bytes = Vec::with_capacity(indices.len() * std::mem::size_of::<u16>());
|
|
|
|
|
for index in indices {
|
|
|
|
|
bytes.extend_from_slice(&index.to_ne_bytes());
|
|
|
|
|
}
|
|
|
|
|
create_host_visible_buffer(
|
|
|
|
|
instance,
|
|
|
|
|
device,
|
|
|
|
|
&bytes,
|
|
|
|
|
vk::BufferUsageFlags::INDEX_BUFFER,
|
|
|
|
|
"triangle index buffer",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_host_visible_buffer(
|
|
|
|
|
instance: &VulkanInstanceProbe,
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
bytes: &[u8],
|
|
|
|
|
usage: vk::BufferUsageFlags,
|
|
|
|
|
context: &'static str,
|
|
|
|
|
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
|
|
|
|
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::<u8>(), 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<u32> {
|
|
|
|
|
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_swapchain_resources(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
swapchain: &VulkanSwapchainProbe,
|
|
|
|
|
command_pool: vk::CommandPool,
|
|
|
|
|
vertex_buffer: &VulkanAllocatedBuffer,
|
|
|
|
|
index_buffer: &VulkanAllocatedBuffer,
|
|
|
|
|
reuse_command_pool: bool,
|
|
|
|
|
) -> Result<VulkanSwapchainResources, VulkanSmokeRendererError> {
|
|
|
|
|
// SAFETY: The swapchain is live and owned by this renderer for the duration of the query.
|
|
|
|
|
let images = unsafe {
|
|
|
|
|
swapchain
|
|
|
|
|
.loader()
|
|
|
|
|
.get_swapchain_images(swapchain.swapchain())
|
|
|
|
|
}
|
|
|
|
|
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkGetSwapchainImagesKHR",
|
|
|
|
|
result: error,
|
|
|
|
|
})?;
|
|
|
|
|
let mut partial = PartialSwapchainResources {
|
2026-06-25 06:57:26 +04:00
|
|
|
image_views: create_swapchain_image_views(
|
|
|
|
|
device,
|
|
|
|
|
&images,
|
|
|
|
|
swapchain.report.plan.format.format,
|
|
|
|
|
)?,
|
2026-06-25 06:33:56 +04:00
|
|
|
render_pass: None,
|
|
|
|
|
pipeline_layout: None,
|
|
|
|
|
pipeline: None,
|
2026-06-25 06:57:26 +04:00
|
|
|
framebuffers: Vec::new(),
|
2026-06-25 06:33:56 +04:00
|
|
|
command_buffers: Vec::new(),
|
|
|
|
|
};
|
2026-06-25 06:57:26 +04:00
|
|
|
let (render_pass, pipeline_layout, pipeline) = match create_swapchain_pipeline_bundle(
|
|
|
|
|
device,
|
|
|
|
|
swapchain.report.plan.format.format,
|
|
|
|
|
swapchain.report.plan.extent,
|
|
|
|
|
) {
|
|
|
|
|
Ok(bundle) => bundle,
|
2026-06-25 06:33:56 +04:00
|
|
|
Err(error) => {
|
|
|
|
|
destroy_partial_swapchain_resources(device, command_pool, partial);
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
partial.render_pass = Some(render_pass);
|
|
|
|
|
partial.pipeline_layout = Some(pipeline_layout);
|
2026-06-25 06:57:26 +04:00
|
|
|
partial.pipeline = Some(pipeline);
|
|
|
|
|
let framebuffers = match create_swapchain_framebuffers(
|
2026-06-25 06:33:56 +04:00
|
|
|
device,
|
|
|
|
|
render_pass,
|
2026-06-25 06:57:26 +04:00
|
|
|
&partial.image_views,
|
2026-06-25 06:33:56 +04:00
|
|
|
swapchain.report.plan.extent,
|
|
|
|
|
) {
|
2026-06-25 06:57:26 +04:00
|
|
|
Ok(framebuffers) => framebuffers,
|
2026-06-25 06:33:56 +04:00
|
|
|
Err(error) => {
|
|
|
|
|
destroy_partial_swapchain_resources(device, command_pool, partial);
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-06-25 06:57:26 +04:00
|
|
|
partial.framebuffers = framebuffers;
|
|
|
|
|
reset_reusable_command_pool(device, command_pool, reuse_command_pool)?;
|
2026-06-25 06:33:56 +04:00
|
|
|
let command_buffers = match allocate_command_buffers(
|
|
|
|
|
device,
|
|
|
|
|
command_pool,
|
|
|
|
|
u32::try_from(images.len()).unwrap_or(u32::MAX),
|
|
|
|
|
) {
|
|
|
|
|
Ok(command_buffers) => command_buffers,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
destroy_partial_swapchain_resources(device, command_pool, partial);
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
partial.command_buffers = command_buffers;
|
|
|
|
|
let _ = (vertex_buffer, index_buffer);
|
|
|
|
|
Ok(VulkanSwapchainResources {
|
|
|
|
|
image_views: partial.image_views,
|
|
|
|
|
render_pass,
|
|
|
|
|
pipeline_layout,
|
|
|
|
|
pipeline,
|
|
|
|
|
framebuffers: partial.framebuffers,
|
|
|
|
|
command_buffers: partial.command_buffers,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 06:57:26 +04:00
|
|
|
fn create_swapchain_image_views(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
images: &[vk::Image],
|
|
|
|
|
format: i32,
|
|
|
|
|
) -> Result<Vec<vk::ImageView>, VulkanSmokeRendererError> {
|
|
|
|
|
let mut image_views = Vec::with_capacity(images.len());
|
|
|
|
|
for image in images.iter().copied() {
|
|
|
|
|
image_views.push(create_image_view(device, image, format)?);
|
|
|
|
|
}
|
|
|
|
|
Ok(image_views)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_swapchain_pipeline_bundle(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
format: i32,
|
|
|
|
|
extent: (u32, u32),
|
|
|
|
|
) -> Result<(vk::RenderPass, vk::PipelineLayout, vk::Pipeline), VulkanSmokeRendererError> {
|
|
|
|
|
let render_pass = create_render_pass(device, format)?;
|
|
|
|
|
let pipeline_layout = create_pipeline_layout(device).inspect_err(|_| {
|
|
|
|
|
// SAFETY: The render pass was created above on this live logical device and is destroyed on setup failure.
|
|
|
|
|
unsafe { device.device().destroy_render_pass(render_pass, None) };
|
|
|
|
|
})?;
|
|
|
|
|
let pipeline = create_graphics_pipeline(device, render_pass, pipeline_layout, extent)
|
|
|
|
|
.inspect_err(|_| {
|
|
|
|
|
// SAFETY: These objects were created above on this live logical device and are destroyed on setup failure.
|
|
|
|
|
unsafe {
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_pipeline_layout(pipeline_layout, None);
|
|
|
|
|
device.device().destroy_render_pass(render_pass, None);
|
|
|
|
|
}
|
|
|
|
|
})?;
|
|
|
|
|
Ok((render_pass, pipeline_layout, pipeline))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_swapchain_framebuffers(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
render_pass: vk::RenderPass,
|
|
|
|
|
image_views: &[vk::ImageView],
|
|
|
|
|
extent: (u32, u32),
|
|
|
|
|
) -> Result<Vec<vk::Framebuffer>, VulkanSmokeRendererError> {
|
|
|
|
|
let mut framebuffers = Vec::with_capacity(image_views.len());
|
|
|
|
|
for image_view in image_views.iter().copied() {
|
|
|
|
|
match create_framebuffer(device, render_pass, image_view, extent) {
|
|
|
|
|
Ok(framebuffer) => framebuffers.push(framebuffer),
|
|
|
|
|
Err(error) => {
|
|
|
|
|
// SAFETY: These framebuffers were created above on this live logical device and are destroyed on setup failure.
|
|
|
|
|
unsafe {
|
|
|
|
|
for framebuffer in framebuffers.iter().copied() {
|
|
|
|
|
device.device().destroy_framebuffer(framebuffer, None);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(framebuffers)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn reset_reusable_command_pool(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
command_pool: vk::CommandPool,
|
|
|
|
|
reuse_command_pool: bool,
|
|
|
|
|
) -> Result<(), VulkanSmokeRendererError> {
|
|
|
|
|
if !reuse_command_pool {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
// SAFETY: All command buffers allocated from the live pool are freed before reallocating them.
|
|
|
|
|
unsafe {
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.reset_command_pool(command_pool, vk::CommandPoolResetFlags::empty())
|
|
|
|
|
}
|
|
|
|
|
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkResetCommandPool",
|
|
|
|
|
result: error,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 06:33:56 +04:00
|
|
|
fn create_image_view(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
image: vk::Image,
|
|
|
|
|
format: i32,
|
|
|
|
|
) -> Result<vk::ImageView, VulkanSmokeRendererError> {
|
|
|
|
|
let create_info = vk::ImageViewCreateInfo::default()
|
|
|
|
|
.image(image)
|
|
|
|
|
.view_type(vk::ImageViewType::TYPE_2D)
|
|
|
|
|
.format(vk::Format::from_raw(format))
|
|
|
|
|
.subresource_range(color_subresource_range());
|
|
|
|
|
// SAFETY: The image comes from the live swapchain and the subresource range covers its color aspect.
|
|
|
|
|
unsafe { device.device().create_image_view(&create_info, None) }.map_err(|error| {
|
|
|
|
|
VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkCreateImageView",
|
|
|
|
|
result: error,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_render_pass(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
format: i32,
|
|
|
|
|
) -> Result<vk::RenderPass, VulkanSmokeRendererError> {
|
|
|
|
|
let color_attachment = vk::AttachmentDescription::default()
|
|
|
|
|
.format(vk::Format::from_raw(format))
|
|
|
|
|
.samples(vk::SampleCountFlags::TYPE_1)
|
|
|
|
|
.load_op(vk::AttachmentLoadOp::CLEAR)
|
|
|
|
|
.store_op(vk::AttachmentStoreOp::STORE)
|
|
|
|
|
.initial_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
|
|
|
|
.final_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
|
|
|
|
|
let color_attachment_ref = vk::AttachmentReference::default()
|
|
|
|
|
.attachment(0)
|
|
|
|
|
.layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
|
|
|
|
|
let color_attachments = [color_attachment_ref];
|
|
|
|
|
let subpass = vk::SubpassDescription::default()
|
|
|
|
|
.pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
|
|
|
|
|
.color_attachments(&color_attachments);
|
|
|
|
|
let dependency = vk::SubpassDependency::default()
|
|
|
|
|
.src_subpass(vk::SUBPASS_EXTERNAL)
|
|
|
|
|
.dst_subpass(0)
|
|
|
|
|
.src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
|
|
|
|
|
.dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
|
|
|
|
|
.dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE);
|
|
|
|
|
let attachments = [color_attachment];
|
|
|
|
|
let subpasses = [subpass];
|
|
|
|
|
let dependencies = [dependency];
|
|
|
|
|
let create_info = vk::RenderPassCreateInfo::default()
|
|
|
|
|
.attachments(&attachments)
|
|
|
|
|
.subpasses(&subpasses)
|
|
|
|
|
.dependencies(&dependencies);
|
|
|
|
|
// SAFETY: The render-pass create info only references stack-owned descriptors.
|
|
|
|
|
unsafe { device.device().create_render_pass(&create_info, None) }.map_err(|error| {
|
|
|
|
|
VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkCreateRenderPass",
|
|
|
|
|
result: error,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_pipeline_layout(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
) -> Result<vk::PipelineLayout, VulkanSmokeRendererError> {
|
|
|
|
|
let create_info = vk::PipelineLayoutCreateInfo::default();
|
|
|
|
|
// SAFETY: The pipeline layout contains no descriptor sets or push constants.
|
|
|
|
|
unsafe { device.device().create_pipeline_layout(&create_info, None) }.map_err(|error| {
|
|
|
|
|
VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkCreatePipelineLayout",
|
|
|
|
|
result: error,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn extent_component_to_f32(value: u32) -> f32 {
|
|
|
|
|
u16::try_from(value).map_or(f32::from(u16::MAX), f32::from)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::too_many_lines)]
|
|
|
|
|
fn create_graphics_pipeline(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
render_pass: vk::RenderPass,
|
|
|
|
|
pipeline_layout: vk::PipelineLayout,
|
|
|
|
|
extent: (u32, u32),
|
|
|
|
|
) -> Result<vk::Pipeline, VulkanSmokeRendererError> {
|
|
|
|
|
let entry_point = c"main";
|
|
|
|
|
let vertex_module = create_shader_module(device, TRIANGLE_VERTEX_SHADER_WORDS)?;
|
|
|
|
|
let fragment_module = match create_shader_module(device, TRIANGLE_FRAGMENT_SHADER_WORDS) {
|
|
|
|
|
Ok(fragment_module) => fragment_module,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
// SAFETY: The vertex shader module was created above on this logical device and is destroyed on setup failure.
|
|
|
|
|
unsafe { device.device().destroy_shader_module(vertex_module, None) };
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let stage_create_infos = [
|
|
|
|
|
vk::PipelineShaderStageCreateInfo::default()
|
|
|
|
|
.stage(vk::ShaderStageFlags::VERTEX)
|
|
|
|
|
.module(vertex_module)
|
|
|
|
|
.name(entry_point),
|
|
|
|
|
vk::PipelineShaderStageCreateInfo::default()
|
|
|
|
|
.stage(vk::ShaderStageFlags::FRAGMENT)
|
|
|
|
|
.module(fragment_module)
|
|
|
|
|
.name(entry_point),
|
|
|
|
|
];
|
|
|
|
|
let binding_descriptions = [vk::VertexInputBindingDescription {
|
|
|
|
|
binding: 0,
|
|
|
|
|
stride: 20,
|
|
|
|
|
input_rate: vk::VertexInputRate::VERTEX,
|
|
|
|
|
}];
|
|
|
|
|
let attribute_descriptions = [
|
|
|
|
|
vk::VertexInputAttributeDescription {
|
|
|
|
|
location: 0,
|
|
|
|
|
binding: 0,
|
|
|
|
|
format: vk::Format::R32G32_SFLOAT,
|
|
|
|
|
offset: 0,
|
|
|
|
|
},
|
|
|
|
|
vk::VertexInputAttributeDescription {
|
|
|
|
|
location: 1,
|
|
|
|
|
binding: 0,
|
|
|
|
|
format: vk::Format::R32G32B32_SFLOAT,
|
|
|
|
|
offset: 8,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
let vertex_input_state = vk::PipelineVertexInputStateCreateInfo::default()
|
|
|
|
|
.vertex_binding_descriptions(&binding_descriptions)
|
|
|
|
|
.vertex_attribute_descriptions(&attribute_descriptions);
|
|
|
|
|
let input_assembly_state = vk::PipelineInputAssemblyStateCreateInfo::default()
|
|
|
|
|
.topology(vk::PrimitiveTopology::TRIANGLE_LIST);
|
|
|
|
|
let viewports = [vk::Viewport {
|
|
|
|
|
x: 0.0,
|
|
|
|
|
y: 0.0,
|
|
|
|
|
width: extent_component_to_f32(extent.0),
|
|
|
|
|
height: extent_component_to_f32(extent.1),
|
|
|
|
|
min_depth: 0.0,
|
|
|
|
|
max_depth: 1.0,
|
|
|
|
|
}];
|
|
|
|
|
let scissors = [vk::Rect2D {
|
|
|
|
|
offset: vk::Offset2D { x: 0, y: 0 },
|
|
|
|
|
extent: vk::Extent2D {
|
|
|
|
|
width: extent.0,
|
|
|
|
|
height: extent.1,
|
|
|
|
|
},
|
|
|
|
|
}];
|
|
|
|
|
let viewport_state = vk::PipelineViewportStateCreateInfo::default()
|
|
|
|
|
.viewports(&viewports)
|
|
|
|
|
.scissors(&scissors);
|
|
|
|
|
let rasterization_state = vk::PipelineRasterizationStateCreateInfo::default()
|
|
|
|
|
.polygon_mode(vk::PolygonMode::FILL)
|
|
|
|
|
.cull_mode(vk::CullModeFlags::BACK)
|
|
|
|
|
.front_face(vk::FrontFace::CLOCKWISE)
|
|
|
|
|
.line_width(1.0);
|
|
|
|
|
let multisample_state = vk::PipelineMultisampleStateCreateInfo::default()
|
|
|
|
|
.rasterization_samples(vk::SampleCountFlags::TYPE_1);
|
|
|
|
|
let color_blend_attachment = [vk::PipelineColorBlendAttachmentState::default()
|
|
|
|
|
.color_write_mask(
|
|
|
|
|
vk::ColorComponentFlags::R
|
|
|
|
|
| vk::ColorComponentFlags::G
|
|
|
|
|
| vk::ColorComponentFlags::B
|
|
|
|
|
| vk::ColorComponentFlags::A,
|
|
|
|
|
)];
|
|
|
|
|
let color_blend_state =
|
|
|
|
|
vk::PipelineColorBlendStateCreateInfo::default().attachments(&color_blend_attachment);
|
|
|
|
|
let create_info = [vk::GraphicsPipelineCreateInfo::default()
|
|
|
|
|
.stages(&stage_create_infos)
|
|
|
|
|
.vertex_input_state(&vertex_input_state)
|
|
|
|
|
.input_assembly_state(&input_assembly_state)
|
|
|
|
|
.viewport_state(&viewport_state)
|
|
|
|
|
.rasterization_state(&rasterization_state)
|
|
|
|
|
.multisample_state(&multisample_state)
|
|
|
|
|
.color_blend_state(&color_blend_state)
|
|
|
|
|
.layout(pipeline_layout)
|
|
|
|
|
.render_pass(render_pass)
|
|
|
|
|
.subpass(0)];
|
|
|
|
|
// SAFETY: The pipeline creation references live shader modules and stack-owned fixed-function descriptors.
|
|
|
|
|
let pipeline_result = unsafe {
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.create_graphics_pipelines(vk::PipelineCache::null(), &create_info, None)
|
|
|
|
|
};
|
|
|
|
|
// SAFETY: Shader modules are no longer needed after pipeline creation completes.
|
|
|
|
|
unsafe {
|
|
|
|
|
device.device().destroy_shader_module(vertex_module, None);
|
|
|
|
|
device.device().destroy_shader_module(fragment_module, None);
|
|
|
|
|
}
|
|
|
|
|
let pipeline =
|
|
|
|
|
pipeline_result.map_err(|(_, error)| VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkCreateGraphicsPipelines",
|
|
|
|
|
result: error,
|
|
|
|
|
})?[0];
|
|
|
|
|
Ok(pipeline)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_shader_module(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
words: &[u32],
|
|
|
|
|
) -> Result<vk::ShaderModule, VulkanSmokeRendererError> {
|
|
|
|
|
let create_info = vk::ShaderModuleCreateInfo::default().code(words);
|
|
|
|
|
// SAFETY: SPIR-V words are immutable and valid for the duration of the call.
|
|
|
|
|
unsafe { device.device().create_shader_module(&create_info, None) }.map_err(|error| {
|
|
|
|
|
VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkCreateShaderModule",
|
|
|
|
|
result: error,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_framebuffer(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
render_pass: vk::RenderPass,
|
|
|
|
|
image_view: vk::ImageView,
|
|
|
|
|
extent: (u32, u32),
|
|
|
|
|
) -> Result<vk::Framebuffer, VulkanSmokeRendererError> {
|
|
|
|
|
let attachments = [image_view];
|
|
|
|
|
let create_info = vk::FramebufferCreateInfo::default()
|
|
|
|
|
.render_pass(render_pass)
|
|
|
|
|
.attachments(&attachments)
|
|
|
|
|
.width(extent.0)
|
|
|
|
|
.height(extent.1)
|
|
|
|
|
.layers(1);
|
|
|
|
|
// SAFETY: The framebuffer attachments and render pass remain live for the duration of the call.
|
|
|
|
|
unsafe { device.device().create_framebuffer(&create_info, None) }.map_err(|error| {
|
|
|
|
|
VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkCreateFramebuffer",
|
|
|
|
|
result: error,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn allocate_command_buffers(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
command_pool: vk::CommandPool,
|
|
|
|
|
count: u32,
|
|
|
|
|
) -> Result<Vec<vk::CommandBuffer>, VulkanSmokeRendererError> {
|
|
|
|
|
let allocate_info = vk::CommandBufferAllocateInfo::default()
|
|
|
|
|
.command_pool(command_pool)
|
|
|
|
|
.level(vk::CommandBufferLevel::PRIMARY)
|
|
|
|
|
.command_buffer_count(count);
|
|
|
|
|
// SAFETY: Command buffers are allocated from a live resettable pool owned by this device.
|
|
|
|
|
unsafe { device.device().allocate_command_buffers(&allocate_info) }.map_err(|error| {
|
|
|
|
|
VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkAllocateCommandBuffers",
|
|
|
|
|
result: error,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn create_frame_sync(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
) -> Result<Vec<VulkanFrameSync>, 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 = unsafe { device.device().create_semaphore(&semaphore_info, None) }
|
|
|
|
|
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkCreateSemaphore(image_available)",
|
|
|
|
|
result: error,
|
|
|
|
|
})?;
|
2026-06-25 06:42:28 +04:00
|
|
|
let render_finished = {
|
|
|
|
|
// SAFETY: The sync objects belong to this live logical device and are destroyed at teardown.
|
2026-06-25 06:33:56 +04:00
|
|
|
match unsafe { device.device().create_semaphore(&semaphore_info, None) } {
|
|
|
|
|
Ok(render_finished) => render_finished,
|
|
|
|
|
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)",
|
|
|
|
|
result: error,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-06-25 06:42:28 +04:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
// SAFETY: The fence belongs to this live logical device and is destroyed at teardown.
|
2026-06-25 06:33:56 +04:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
return Err(VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkCreateFence",
|
|
|
|
|
result: error,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
sync.push(VulkanFrameSync {
|
|
|
|
|
image_available,
|
|
|
|
|
render_finished,
|
|
|
|
|
fence,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
Ok(sync)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn destroy_swapchain_resources(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
command_pool: vk::CommandPool,
|
|
|
|
|
resources: VulkanSwapchainResources,
|
|
|
|
|
) {
|
|
|
|
|
// SAFETY: All swapchain-dependent objects belong to this device and are destroyed once.
|
|
|
|
|
unsafe {
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.free_command_buffers(command_pool, &resources.command_buffers);
|
|
|
|
|
for framebuffer in resources.framebuffers {
|
|
|
|
|
device.device().destroy_framebuffer(framebuffer, None);
|
|
|
|
|
}
|
|
|
|
|
device.device().destroy_pipeline(resources.pipeline, None);
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_pipeline_layout(resources.pipeline_layout, None);
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_render_pass(resources.render_pass, None);
|
|
|
|
|
for image_view in resources.image_views {
|
|
|
|
|
device.device().destroy_image_view(image_view, None);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn destroy_partial_swapchain_resources(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
command_pool: vk::CommandPool,
|
|
|
|
|
resources: PartialSwapchainResources,
|
|
|
|
|
) {
|
|
|
|
|
// SAFETY: All handles in this partial resource set were created on this live logical device and are destroyed once.
|
|
|
|
|
unsafe {
|
|
|
|
|
if !resources.command_buffers.is_empty() {
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.free_command_buffers(command_pool, &resources.command_buffers);
|
|
|
|
|
}
|
|
|
|
|
for framebuffer in resources.framebuffers {
|
|
|
|
|
device.device().destroy_framebuffer(framebuffer, None);
|
|
|
|
|
}
|
|
|
|
|
if let Some(pipeline) = resources.pipeline {
|
|
|
|
|
device.device().destroy_pipeline(pipeline, None);
|
|
|
|
|
}
|
|
|
|
|
if let Some(pipeline_layout) = resources.pipeline_layout {
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_pipeline_layout(pipeline_layout, None);
|
|
|
|
|
}
|
|
|
|
|
if let Some(render_pass) = resources.render_pass {
|
|
|
|
|
device.device().destroy_render_pass(render_pass, None);
|
|
|
|
|
}
|
|
|
|
|
for image_view in resources.image_views {
|
|
|
|
|
device.device().destroy_image_view(image_view, None);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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_semaphore(frame_sync.render_finished, 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn color_subresource_range() -> vk::ImageSubresourceRange {
|
|
|
|
|
vk::ImageSubresourceRange::default()
|
|
|
|
|
.aspect_mask(vk::ImageAspectFlags::COLOR)
|
|
|
|
|
.base_mip_level(0)
|
|
|
|
|
.level_count(1)
|
|
|
|
|
.base_array_layer(0)
|
|
|
|
|
.layer_count(1)
|
|
|
|
|
}
|