2026-06-25 07:04:22 +04:00
|
|
|
#![allow(unsafe_code)]
|
|
|
|
|
|
|
|
|
|
use ash::vk;
|
2026-07-18 08:31:19 +04:00
|
|
|
use fparkan_platform::DepthStencilSupport;
|
2026-07-18 08:22:29 +04:00
|
|
|
use fparkan_render::{
|
|
|
|
|
LegacyBlendMode, LegacyCullMode, LegacyDepthMode, LegacyPipelineState, PipelineKey,
|
|
|
|
|
};
|
|
|
|
|
use std::collections::BTreeMap;
|
2026-06-25 07:04:22 +04:00
|
|
|
|
|
|
|
|
use super::{
|
2026-10-11 13:12:50 +04:00
|
|
|
color_subresource_range, create_depth_attachment, create_material_specular_buffer,
|
|
|
|
|
destroy_depth_attachment, VulkanAllocatedBuffer, VulkanAllocatedImage, VulkanDepthAttachment,
|
|
|
|
|
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeRendererError, VulkanSwapchainProbe,
|
|
|
|
|
TRIANGLE_FRAGMENT_SHADER_WORDS, TRIANGLE_VERTEX_SHADER_WORDS, VULKAN_FRAME_UNIFORM_BYTES,
|
|
|
|
|
VULKAN_MATERIAL_SPECULAR_RECORD_BYTES,
|
2026-06-25 07:04:22 +04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
pub(super) struct VulkanSwapchainResources {
|
|
|
|
|
pub(super) image_views: Vec<vk::ImageView>,
|
2026-07-18 09:05:20 +04:00
|
|
|
pub(super) images: Vec<vk::Image>,
|
|
|
|
|
pub(super) readback_buffers: Vec<VulkanAllocatedBuffer>,
|
2026-10-11 13:12:50 +04:00
|
|
|
/// One depth image per swapchain image so frames can overlap safely.
|
|
|
|
|
pub(super) depth_attachments: Vec<VulkanDepthAttachment>,
|
|
|
|
|
/// A render-finished semaphore dedicated to each swapchain image.
|
|
|
|
|
pub(super) render_finished: Vec<vk::Semaphore>,
|
2026-06-25 07:04:22 +04:00
|
|
|
pub(super) render_pass: vk::RenderPass,
|
|
|
|
|
pub(super) pipeline_layout: vk::PipelineLayout,
|
2026-07-18 07:39:38 +04:00
|
|
|
pub(super) descriptor_set_layout: vk::DescriptorSetLayout,
|
|
|
|
|
pub(super) descriptor_pool: vk::DescriptorPool,
|
2026-07-18 08:07:45 +04:00
|
|
|
pub(super) descriptor_sets: Vec<vk::DescriptorSet>,
|
2026-07-18 07:39:38 +04:00
|
|
|
pub(super) sampler: vk::Sampler,
|
2026-10-11 13:12:50 +04:00
|
|
|
pub(super) frame_uniform_buffer: VulkanAllocatedBuffer,
|
|
|
|
|
pub(super) frame_uniform_stride: u64,
|
|
|
|
|
pub(super) material_specular_buffer: VulkanAllocatedBuffer,
|
|
|
|
|
pub(super) material_specular_stride: u64,
|
2026-07-18 08:22:29 +04:00
|
|
|
/// Live graphics pipelines indexed by canonical backend-neutral state.
|
|
|
|
|
pub(super) pipelines: BTreeMap<PipelineKey, vk::Pipeline>,
|
2026-06-25 07:04:22 +04:00
|
|
|
pub(super) framebuffers: Vec<vk::Framebuffer>,
|
|
|
|
|
pub(super) command_buffers: Vec<vk::CommandBuffer>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 13:12:50 +04:00
|
|
|
pub(super) fn update_material_texture_descriptor(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
resources: &VulkanSwapchainResources,
|
|
|
|
|
material_index: usize,
|
|
|
|
|
texture_slot: usize,
|
|
|
|
|
texture: &VulkanAllocatedImage,
|
|
|
|
|
) -> Result<(), VulkanSmokeRendererError> {
|
|
|
|
|
if texture_slot >= 4 {
|
|
|
|
|
return Err(VulkanSmokeRendererError::InvalidStaticTexture {
|
|
|
|
|
context: "dynamic material texture slot is out of bounds",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
let descriptor_set = resources
|
|
|
|
|
.descriptor_sets
|
|
|
|
|
.get(material_index)
|
|
|
|
|
.copied()
|
|
|
|
|
.ok_or(VulkanSmokeRendererError::InvalidStaticTexture {
|
|
|
|
|
context: "dynamic material descriptor set is out of bounds",
|
|
|
|
|
})?;
|
|
|
|
|
let image_info = vk::DescriptorImageInfo::default()
|
|
|
|
|
.sampler(resources.sampler)
|
|
|
|
|
.image_view(texture.view)
|
|
|
|
|
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
|
|
|
|
|
let write = vk::WriteDescriptorSet::default()
|
|
|
|
|
.dst_set(descriptor_set)
|
|
|
|
|
.dst_binding(u32::try_from(texture_slot).unwrap_or(u32::MAX))
|
|
|
|
|
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
|
|
|
|
.image_info(std::slice::from_ref(&image_info));
|
|
|
|
|
// SAFETY: the renderer idles the device before changing a descriptor that
|
|
|
|
|
// may be referenced by an in-flight command buffer.
|
|
|
|
|
unsafe { device.device().update_descriptor_sets(&[write], &[]) };
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 07:04:22 +04:00
|
|
|
struct PartialSwapchainResources {
|
|
|
|
|
image_views: Vec<vk::ImageView>,
|
2026-07-18 09:05:20 +04:00
|
|
|
readback_buffers: Vec<VulkanAllocatedBuffer>,
|
2026-10-11 13:12:50 +04:00
|
|
|
depth_attachments: Vec<VulkanDepthAttachment>,
|
|
|
|
|
render_finished: Vec<vk::Semaphore>,
|
2026-06-25 07:04:22 +04:00
|
|
|
render_pass: Option<vk::RenderPass>,
|
|
|
|
|
pipeline_layout: Option<vk::PipelineLayout>,
|
2026-07-18 07:39:38 +04:00
|
|
|
descriptor_set_layout: Option<vk::DescriptorSetLayout>,
|
|
|
|
|
descriptor_pool: Option<vk::DescriptorPool>,
|
|
|
|
|
sampler: Option<vk::Sampler>,
|
2026-10-11 13:12:50 +04:00
|
|
|
frame_uniform_buffer: Option<VulkanAllocatedBuffer>,
|
|
|
|
|
frame_uniform_stride: u64,
|
|
|
|
|
material_specular_buffer: Option<VulkanAllocatedBuffer>,
|
|
|
|
|
material_specular_stride: u64,
|
2026-07-18 08:22:29 +04:00
|
|
|
pipelines: BTreeMap<PipelineKey, vk::Pipeline>,
|
2026-06-25 07:04:22 +04:00
|
|
|
framebuffers: Vec<vk::Framebuffer>,
|
|
|
|
|
command_buffers: Vec<vk::CommandBuffer>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn create_swapchain_resources(
|
2026-07-18 08:31:19 +04:00
|
|
|
instance: &VulkanInstanceProbe,
|
2026-06-25 07:04:22 +04:00
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
swapchain: &VulkanSwapchainProbe,
|
|
|
|
|
command_pool: vk::CommandPool,
|
|
|
|
|
vertex_buffer: &VulkanAllocatedBuffer,
|
|
|
|
|
index_buffer: &VulkanAllocatedBuffer,
|
2026-07-18 08:07:45 +04:00
|
|
|
textures: &[VulkanAllocatedImage],
|
2026-10-11 13:12:50 +04:00
|
|
|
texture_slot_indices: &[usize],
|
2026-07-18 08:22:29 +04:00
|
|
|
draw_ranges: &[super::VulkanStaticDrawRange],
|
2026-07-18 08:31:19 +04:00
|
|
|
depth_request: DepthStencilSupport,
|
2026-06-25 07:04:22 +04:00
|
|
|
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 {
|
|
|
|
|
image_views: create_swapchain_image_views(
|
|
|
|
|
device,
|
|
|
|
|
&images,
|
|
|
|
|
swapchain.report.plan.format.format,
|
|
|
|
|
)?,
|
2026-07-18 09:05:20 +04:00
|
|
|
readback_buffers: Vec::new(),
|
2026-10-11 13:12:50 +04:00
|
|
|
depth_attachments: Vec::new(),
|
|
|
|
|
render_finished: Vec::new(),
|
2026-06-25 07:04:22 +04:00
|
|
|
render_pass: None,
|
|
|
|
|
pipeline_layout: None,
|
2026-07-18 07:39:38 +04:00
|
|
|
descriptor_set_layout: None,
|
|
|
|
|
descriptor_pool: None,
|
|
|
|
|
sampler: None,
|
2026-10-11 13:12:50 +04:00
|
|
|
frame_uniform_buffer: None,
|
|
|
|
|
frame_uniform_stride: 0,
|
|
|
|
|
material_specular_buffer: None,
|
|
|
|
|
material_specular_stride: 0,
|
2026-07-18 08:22:29 +04:00
|
|
|
pipelines: BTreeMap::new(),
|
2026-06-25 07:04:22 +04:00
|
|
|
framebuffers: Vec::new(),
|
|
|
|
|
command_buffers: Vec::new(),
|
|
|
|
|
};
|
2026-10-11 13:12:50 +04:00
|
|
|
let semaphore_info = vk::SemaphoreCreateInfo::default();
|
|
|
|
|
for _ in &images {
|
|
|
|
|
// SAFETY: The semaphore belongs to this live logical device and is
|
|
|
|
|
// destroyed with the swapchain resources.
|
|
|
|
|
match unsafe { device.device().create_semaphore(&semaphore_info, None) } {
|
|
|
|
|
Ok(semaphore) => partial.render_finished.push(semaphore),
|
|
|
|
|
Err(result) => {
|
|
|
|
|
destroy_partial_swapchain_resources(device, command_pool, partial);
|
|
|
|
|
return Err(VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkCreateSemaphore(render_finished)",
|
|
|
|
|
result,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let (frame_uniform_buffer, frame_uniform_stride) =
|
|
|
|
|
match super::create_frame_uniform_buffer(instance, device) {
|
|
|
|
|
Ok(buffer) => buffer,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
destroy_partial_swapchain_resources(device, command_pool, partial);
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
partial.frame_uniform_stride = frame_uniform_stride;
|
|
|
|
|
partial.frame_uniform_buffer = Some(frame_uniform_buffer);
|
|
|
|
|
let material_count = texture_slot_indices.len() / 4;
|
|
|
|
|
let (material_specular_buffer, material_specular_stride) =
|
|
|
|
|
match create_material_specular_buffer(instance, device, material_count) {
|
|
|
|
|
Ok(buffer) => buffer,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
destroy_partial_swapchain_resources(device, command_pool, partial);
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
partial.material_specular_stride = material_specular_stride;
|
|
|
|
|
partial.material_specular_buffer = Some(material_specular_buffer);
|
2026-07-18 07:39:38 +04:00
|
|
|
let (
|
2026-07-18 08:31:19 +04:00
|
|
|
depth_attachment,
|
2026-07-18 07:39:38 +04:00
|
|
|
render_pass,
|
|
|
|
|
pipeline_layout,
|
2026-07-18 08:22:29 +04:00
|
|
|
pipelines,
|
2026-07-18 07:39:38 +04:00
|
|
|
descriptor_set_layout,
|
|
|
|
|
descriptor_pool,
|
2026-07-18 08:07:45 +04:00
|
|
|
descriptor_sets,
|
2026-07-18 07:39:38 +04:00
|
|
|
sampler,
|
|
|
|
|
) = match create_swapchain_pipeline_bundle(
|
2026-07-18 08:31:19 +04:00
|
|
|
instance,
|
2026-06-25 07:04:22 +04:00
|
|
|
device,
|
|
|
|
|
swapchain.report.plan.format.format,
|
|
|
|
|
swapchain.report.plan.extent,
|
2026-07-18 08:07:45 +04:00
|
|
|
textures,
|
2026-10-11 13:12:50 +04:00
|
|
|
texture_slot_indices,
|
2026-07-18 08:22:29 +04:00
|
|
|
draw_ranges,
|
2026-07-18 08:31:19 +04:00
|
|
|
depth_request,
|
2026-10-11 13:12:50 +04:00
|
|
|
partial
|
|
|
|
|
.frame_uniform_buffer
|
|
|
|
|
.as_ref()
|
|
|
|
|
.expect("frame uniform buffer is present before descriptor creation"),
|
|
|
|
|
partial
|
|
|
|
|
.material_specular_buffer
|
|
|
|
|
.as_ref()
|
|
|
|
|
.expect("material specular buffer is present before descriptor creation"),
|
2026-06-25 07:04:22 +04:00
|
|
|
) {
|
|
|
|
|
Ok(bundle) => bundle,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
destroy_partial_swapchain_resources(device, command_pool, partial);
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-10-11 13:12:50 +04:00
|
|
|
// Keep every handle from the bundle in the partial owner before creating
|
|
|
|
|
// the remaining per-image depth attachments. That loop can fail after the
|
|
|
|
|
// first attachment, and its error path must destroy the whole bundle.
|
|
|
|
|
partial.depth_attachments.push(depth_attachment);
|
2026-06-25 07:04:22 +04:00
|
|
|
partial.render_pass = Some(render_pass);
|
|
|
|
|
partial.pipeline_layout = Some(pipeline_layout);
|
2026-07-18 07:39:38 +04:00
|
|
|
partial.descriptor_set_layout = Some(descriptor_set_layout);
|
|
|
|
|
partial.descriptor_pool = Some(descriptor_pool);
|
|
|
|
|
partial.sampler = Some(sampler);
|
2026-07-18 08:22:29 +04:00
|
|
|
partial.pipelines = pipelines;
|
2026-10-11 13:12:50 +04:00
|
|
|
for _ in 1..images.len() {
|
|
|
|
|
match create_depth_attachment(
|
|
|
|
|
instance,
|
|
|
|
|
device,
|
|
|
|
|
swapchain.report.plan.extent,
|
|
|
|
|
depth_request,
|
|
|
|
|
) {
|
|
|
|
|
Ok(attachment) => partial.depth_attachments.push(attachment),
|
|
|
|
|
Err(error) => {
|
|
|
|
|
destroy_partial_swapchain_resources(device, command_pool, partial);
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-25 07:04:22 +04:00
|
|
|
let framebuffers = match create_swapchain_framebuffers(
|
|
|
|
|
device,
|
|
|
|
|
render_pass,
|
|
|
|
|
&partial.image_views,
|
2026-10-11 13:12:50 +04:00
|
|
|
&partial.depth_attachments,
|
2026-06-25 07:04:22 +04:00
|
|
|
swapchain.report.plan.extent,
|
|
|
|
|
) {
|
|
|
|
|
Ok(framebuffers) => framebuffers,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
destroy_partial_swapchain_resources(device, command_pool, partial);
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
partial.framebuffers = framebuffers;
|
2026-07-18 09:05:20 +04:00
|
|
|
if vk::ImageUsageFlags::from_raw(swapchain.report.plan.image_usage)
|
|
|
|
|
.contains(vk::ImageUsageFlags::TRANSFER_SRC)
|
|
|
|
|
{
|
|
|
|
|
let byte_len = usize::try_from(swapchain.report.plan.extent.0)
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|width| {
|
|
|
|
|
usize::try_from(swapchain.report.plan.extent.1)
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|height| width.checked_mul(height))
|
|
|
|
|
})
|
|
|
|
|
.and_then(|pixels| pixels.checked_mul(4))
|
|
|
|
|
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
|
|
|
|
|
context: "swapchain readback byte length",
|
|
|
|
|
})?;
|
|
|
|
|
for _ in &images {
|
|
|
|
|
match super::create_readback_buffer(instance, device, byte_len) {
|
|
|
|
|
Ok(buffer) => partial.readback_buffers.push(buffer),
|
|
|
|
|
Err(error) => {
|
|
|
|
|
destroy_partial_swapchain_resources(device, command_pool, partial);
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-25 07:04:22 +04:00
|
|
|
reset_reusable_command_pool(device, command_pool, reuse_command_pool)?;
|
|
|
|
|
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);
|
2026-10-11 13:12:50 +04:00
|
|
|
if partial.depth_attachments.len() != images.len()
|
|
|
|
|
|| partial.render_finished.len() != images.len()
|
|
|
|
|
{
|
|
|
|
|
destroy_partial_swapchain_resources(device, command_pool, partial);
|
|
|
|
|
return Err(VulkanSmokeRendererError::InvariantViolation {
|
|
|
|
|
context: "swapchain per-image resource count after setup",
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-06-25 07:04:22 +04:00
|
|
|
Ok(VulkanSwapchainResources {
|
|
|
|
|
image_views: partial.image_views,
|
2026-07-18 09:05:20 +04:00
|
|
|
images,
|
|
|
|
|
readback_buffers: partial.readback_buffers,
|
2026-10-11 13:12:50 +04:00
|
|
|
depth_attachments: partial.depth_attachments,
|
|
|
|
|
render_finished: partial.render_finished,
|
|
|
|
|
render_pass: partial
|
|
|
|
|
.render_pass
|
|
|
|
|
.expect("render pass is present after successful swapchain setup"),
|
|
|
|
|
pipeline_layout: partial
|
|
|
|
|
.pipeline_layout
|
|
|
|
|
.expect("pipeline layout is present after successful swapchain setup"),
|
|
|
|
|
descriptor_set_layout: partial
|
|
|
|
|
.descriptor_set_layout
|
|
|
|
|
.expect("descriptor set layout is present after successful swapchain setup"),
|
|
|
|
|
descriptor_pool: partial
|
|
|
|
|
.descriptor_pool
|
|
|
|
|
.expect("descriptor pool is present after successful swapchain setup"),
|
2026-07-18 08:07:45 +04:00
|
|
|
descriptor_sets,
|
2026-10-11 13:12:50 +04:00
|
|
|
sampler: partial
|
|
|
|
|
.sampler
|
|
|
|
|
.expect("sampler is present after successful swapchain setup"),
|
|
|
|
|
frame_uniform_buffer: partial
|
|
|
|
|
.frame_uniform_buffer
|
|
|
|
|
.expect("frame uniform buffer is present after successful swapchain setup"),
|
|
|
|
|
frame_uniform_stride: partial.frame_uniform_stride,
|
|
|
|
|
material_specular_buffer: partial
|
|
|
|
|
.material_specular_buffer
|
|
|
|
|
.expect("material specular buffer is present after successful swapchain setup"),
|
|
|
|
|
material_specular_stride: partial.material_specular_stride,
|
2026-07-18 08:22:29 +04:00
|
|
|
pipelines: partial.pipelines,
|
2026-06-25 07:04:22 +04:00
|
|
|
framebuffers: partial.framebuffers,
|
|
|
|
|
command_buffers: partial.command_buffers,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(
|
2026-07-18 08:31:19 +04:00
|
|
|
instance: &VulkanInstanceProbe,
|
2026-06-25 07:04:22 +04:00
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
format: i32,
|
|
|
|
|
extent: (u32, u32),
|
2026-07-18 08:07:45 +04:00
|
|
|
textures: &[VulkanAllocatedImage],
|
2026-10-11 13:12:50 +04:00
|
|
|
texture_slot_indices: &[usize],
|
2026-07-18 08:22:29 +04:00
|
|
|
draw_ranges: &[super::VulkanStaticDrawRange],
|
2026-07-18 08:31:19 +04:00
|
|
|
depth_request: DepthStencilSupport,
|
2026-10-11 13:12:50 +04:00
|
|
|
frame_uniform_buffer: &VulkanAllocatedBuffer,
|
|
|
|
|
material_specular_buffer: &VulkanAllocatedBuffer,
|
2026-07-18 07:39:38 +04:00
|
|
|
) -> Result<
|
|
|
|
|
(
|
2026-07-18 08:31:19 +04:00
|
|
|
VulkanDepthAttachment,
|
2026-07-18 07:39:38 +04:00
|
|
|
vk::RenderPass,
|
|
|
|
|
vk::PipelineLayout,
|
2026-07-18 08:22:29 +04:00
|
|
|
BTreeMap<PipelineKey, vk::Pipeline>,
|
2026-07-18 07:39:38 +04:00
|
|
|
vk::DescriptorSetLayout,
|
|
|
|
|
vk::DescriptorPool,
|
2026-07-18 08:07:45 +04:00
|
|
|
Vec<vk::DescriptorSet>,
|
2026-07-18 07:39:38 +04:00
|
|
|
vk::Sampler,
|
|
|
|
|
),
|
|
|
|
|
VulkanSmokeRendererError,
|
|
|
|
|
> {
|
2026-07-18 08:31:19 +04:00
|
|
|
let depth_attachment = create_depth_attachment(instance, device, extent, depth_request)?;
|
|
|
|
|
let render_pass = match create_render_pass(device, format, depth_attachment.format) {
|
|
|
|
|
Ok(render_pass) => render_pass,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
destroy_depth_attachment(device, &depth_attachment);
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-07-18 08:07:45 +04:00
|
|
|
let (descriptor_set_layout, descriptor_pool, descriptor_sets, sampler) =
|
2026-10-11 13:12:50 +04:00
|
|
|
create_texture_descriptor_bundle(
|
|
|
|
|
device,
|
|
|
|
|
textures,
|
|
|
|
|
texture_slot_indices,
|
|
|
|
|
frame_uniform_buffer,
|
|
|
|
|
material_specular_buffer,
|
|
|
|
|
)
|
|
|
|
|
.inspect_err(|_| {
|
2026-07-18 07:39:38 +04:00
|
|
|
// 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) };
|
2026-07-18 08:31:19 +04:00
|
|
|
destroy_depth_attachment(device, &depth_attachment);
|
2026-07-18 07:39:38 +04:00
|
|
|
})?;
|
|
|
|
|
let pipeline_layout =
|
|
|
|
|
create_pipeline_layout(device, descriptor_set_layout).inspect_err(|_| {
|
|
|
|
|
// SAFETY: The descriptor resources and render pass were created above and are rolled back on this device.
|
|
|
|
|
unsafe {
|
|
|
|
|
device.device().destroy_sampler(sampler, None);
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_descriptor_pool(descriptor_pool, None);
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_descriptor_set_layout(descriptor_set_layout, None);
|
|
|
|
|
device.device().destroy_render_pass(render_pass, None);
|
|
|
|
|
}
|
2026-07-18 08:31:19 +04:00
|
|
|
destroy_depth_attachment(device, &depth_attachment);
|
2026-07-18 07:39:38 +04:00
|
|
|
})?;
|
2026-07-18 08:22:29 +04:00
|
|
|
let pipelines =
|
|
|
|
|
create_graphics_pipeline_cache(device, render_pass, pipeline_layout, extent, draw_ranges)
|
|
|
|
|
.inspect_err(|_| {
|
2026-06-25 07:04:22 +04:00
|
|
|
// 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);
|
2026-07-18 07:39:38 +04:00
|
|
|
device.device().destroy_sampler(sampler, None);
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_descriptor_pool(descriptor_pool, None);
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_descriptor_set_layout(descriptor_set_layout, None);
|
2026-06-25 07:04:22 +04:00
|
|
|
device.device().destroy_render_pass(render_pass, None);
|
|
|
|
|
}
|
2026-07-18 08:31:19 +04:00
|
|
|
destroy_depth_attachment(device, &depth_attachment);
|
2026-06-25 07:04:22 +04:00
|
|
|
})?;
|
2026-07-18 07:39:38 +04:00
|
|
|
Ok((
|
2026-07-18 08:31:19 +04:00
|
|
|
depth_attachment,
|
2026-07-18 07:39:38 +04:00
|
|
|
render_pass,
|
|
|
|
|
pipeline_layout,
|
2026-07-18 08:22:29 +04:00
|
|
|
pipelines,
|
2026-07-18 07:39:38 +04:00
|
|
|
descriptor_set_layout,
|
|
|
|
|
descriptor_pool,
|
2026-07-18 08:07:45 +04:00
|
|
|
descriptor_sets,
|
2026-07-18 07:39:38 +04:00
|
|
|
sampler,
|
|
|
|
|
))
|
2026-06-25 07:04:22 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_swapchain_framebuffers(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
render_pass: vk::RenderPass,
|
|
|
|
|
image_views: &[vk::ImageView],
|
2026-10-11 13:12:50 +04:00
|
|
|
depth_attachments: &[VulkanDepthAttachment],
|
2026-06-25 07:04:22 +04:00
|
|
|
extent: (u32, u32),
|
|
|
|
|
) -> Result<Vec<vk::Framebuffer>, VulkanSmokeRendererError> {
|
2026-10-11 13:12:50 +04:00
|
|
|
if image_views.len() != depth_attachments.len() {
|
|
|
|
|
return Err(VulkanSmokeRendererError::InvariantViolation {
|
|
|
|
|
context: "swapchain color/depth attachment count",
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-06-25 07:04:22 +04:00
|
|
|
let mut framebuffers = Vec::with_capacity(image_views.len());
|
2026-10-11 13:12:50 +04:00
|
|
|
for (image_view, depth_attachment) in image_views.iter().copied().zip(depth_attachments) {
|
|
|
|
|
match create_framebuffer(
|
|
|
|
|
device,
|
|
|
|
|
render_pass,
|
|
|
|
|
image_view,
|
|
|
|
|
depth_attachment.image.view,
|
|
|
|
|
extent,
|
|
|
|
|
) {
|
2026-06-25 07:04:22 +04:00
|
|
|
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,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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))
|
2026-10-11 13:12:50 +04:00
|
|
|
.subresource_range(color_subresource_range(1));
|
2026-06-25 07:04:22 +04:00
|
|
|
// 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,
|
2026-07-18 08:31:19 +04:00
|
|
|
depth_format: vk::Format,
|
2026-06-25 07:04:22 +04:00
|
|
|
) -> 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)
|
2026-06-25 07:14:16 +04:00
|
|
|
.initial_layout(vk::ImageLayout::UNDEFINED)
|
|
|
|
|
.final_layout(vk::ImageLayout::PRESENT_SRC_KHR);
|
2026-06-25 07:04:22 +04:00
|
|
|
let color_attachment_ref = vk::AttachmentReference::default()
|
|
|
|
|
.attachment(0)
|
|
|
|
|
.layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
|
2026-07-18 08:31:19 +04:00
|
|
|
let depth_attachment = vk::AttachmentDescription::default()
|
|
|
|
|
.format(depth_format)
|
|
|
|
|
.samples(vk::SampleCountFlags::TYPE_1)
|
|
|
|
|
.load_op(vk::AttachmentLoadOp::CLEAR)
|
|
|
|
|
.store_op(vk::AttachmentStoreOp::DONT_CARE)
|
|
|
|
|
.stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
|
|
|
|
|
.stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
|
|
|
|
|
.initial_layout(vk::ImageLayout::UNDEFINED)
|
|
|
|
|
.final_layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
|
|
|
|
|
let depth_attachment_ref = vk::AttachmentReference::default()
|
|
|
|
|
.attachment(1)
|
|
|
|
|
.layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
|
2026-06-25 07:04:22 +04:00
|
|
|
let color_attachments = [color_attachment_ref];
|
|
|
|
|
let subpass = vk::SubpassDescription::default()
|
|
|
|
|
.pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
|
2026-07-18 08:31:19 +04:00
|
|
|
.color_attachments(&color_attachments)
|
|
|
|
|
.depth_stencil_attachment(&depth_attachment_ref);
|
2026-06-25 07:04:22 +04:00
|
|
|
let dependency = vk::SubpassDependency::default()
|
|
|
|
|
.src_subpass(vk::SUBPASS_EXTERNAL)
|
|
|
|
|
.dst_subpass(0)
|
2026-10-11 13:12:50 +04:00
|
|
|
.src_stage_mask(
|
|
|
|
|
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
|
|
|
|
|
| vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS
|
|
|
|
|
| vk::PipelineStageFlags::LATE_FRAGMENT_TESTS,
|
|
|
|
|
)
|
|
|
|
|
.dst_stage_mask(
|
|
|
|
|
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
|
|
|
|
|
| vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS
|
|
|
|
|
| vk::PipelineStageFlags::LATE_FRAGMENT_TESTS,
|
|
|
|
|
)
|
2026-06-25 07:14:16 +04:00
|
|
|
.src_access_mask(vk::AccessFlags::empty())
|
2026-10-11 13:12:50 +04:00
|
|
|
.dst_access_mask(
|
|
|
|
|
vk::AccessFlags::COLOR_ATTACHMENT_WRITE
|
|
|
|
|
| vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE,
|
|
|
|
|
);
|
2026-07-18 08:31:19 +04:00
|
|
|
let attachments = [color_attachment, depth_attachment];
|
2026-06-25 07:04:22 +04:00
|
|
|
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,
|
2026-07-18 07:39:38 +04:00
|
|
|
descriptor_set_layout: vk::DescriptorSetLayout,
|
2026-06-25 07:04:22 +04:00
|
|
|
) -> Result<vk::PipelineLayout, VulkanSmokeRendererError> {
|
2026-07-18 07:39:38 +04:00
|
|
|
let set_layouts = [descriptor_set_layout];
|
2026-10-11 13:12:50 +04:00
|
|
|
// Material data occupies one 128-byte push-constant allocation shared by
|
|
|
|
|
// both stages; the vertex stage also reads its specular pair from the
|
|
|
|
|
// dynamic storage buffer at descriptor binding 5. Camera data is supplied
|
|
|
|
|
// by the dynamic frame UBO at descriptor binding 4.
|
|
|
|
|
// Expose the complete block from offset zero so drivers validate the
|
|
|
|
|
// fragment member's declared offset against the pipeline layout.
|
|
|
|
|
let push_constant_ranges = [vk::PushConstantRange::default()
|
|
|
|
|
.stage_flags(vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT)
|
|
|
|
|
.offset(0)
|
|
|
|
|
.size(32 * u32::try_from(std::mem::size_of::<f32>()).unwrap_or(u32::MAX))];
|
2026-07-18 08:40:40 +04:00
|
|
|
let create_info = vk::PipelineLayoutCreateInfo::default()
|
|
|
|
|
.set_layouts(&set_layouts)
|
|
|
|
|
.push_constant_ranges(&push_constant_ranges);
|
2026-07-18 07:39:38 +04:00
|
|
|
// SAFETY: The descriptor-set layout belongs to this live logical device.
|
2026-06-25 07:04:22 +04:00
|
|
|
unsafe { device.device().create_pipeline_layout(&create_info, None) }.map_err(|error| {
|
|
|
|
|
VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkCreatePipelineLayout",
|
|
|
|
|
result: error,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 07:39:38 +04:00
|
|
|
fn create_texture_descriptor_bundle(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
2026-07-18 08:07:45 +04:00
|
|
|
textures: &[VulkanAllocatedImage],
|
2026-10-11 13:12:50 +04:00
|
|
|
texture_slot_indices: &[usize],
|
|
|
|
|
frame_uniform_buffer: &VulkanAllocatedBuffer,
|
|
|
|
|
material_specular_buffer: &VulkanAllocatedBuffer,
|
2026-07-18 07:39:38 +04:00
|
|
|
) -> Result<
|
|
|
|
|
(
|
|
|
|
|
vk::DescriptorSetLayout,
|
|
|
|
|
vk::DescriptorPool,
|
2026-07-18 08:07:45 +04:00
|
|
|
Vec<vk::DescriptorSet>,
|
2026-07-18 07:39:38 +04:00
|
|
|
vk::Sampler,
|
|
|
|
|
),
|
|
|
|
|
VulkanSmokeRendererError,
|
|
|
|
|
> {
|
2026-10-11 13:12:50 +04:00
|
|
|
const TEXTURES_PER_MATERIAL: usize = 4;
|
|
|
|
|
const FRAME_BINDING: u32 = 4;
|
|
|
|
|
const MATERIAL_SPECULAR_BINDING: u32 = 5;
|
|
|
|
|
let mut bindings = (0..TEXTURES_PER_MATERIAL)
|
|
|
|
|
.map(|binding| {
|
|
|
|
|
vk::DescriptorSetLayoutBinding::default()
|
|
|
|
|
.binding(u32::try_from(binding).unwrap_or(u32::MAX))
|
|
|
|
|
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
|
|
|
|
.descriptor_count(1)
|
|
|
|
|
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
bindings.push(
|
|
|
|
|
vk::DescriptorSetLayoutBinding::default()
|
|
|
|
|
.binding(FRAME_BINDING)
|
|
|
|
|
.descriptor_type(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
|
|
|
|
|
.descriptor_count(1)
|
|
|
|
|
.stage_flags(vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT),
|
|
|
|
|
);
|
|
|
|
|
bindings.push(
|
|
|
|
|
vk::DescriptorSetLayoutBinding::default()
|
|
|
|
|
.binding(MATERIAL_SPECULAR_BINDING)
|
|
|
|
|
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER_DYNAMIC)
|
|
|
|
|
.descriptor_count(1)
|
|
|
|
|
.stage_flags(vk::ShaderStageFlags::VERTEX),
|
|
|
|
|
);
|
2026-07-18 07:39:38 +04:00
|
|
|
let layout_info = vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings);
|
|
|
|
|
// SAFETY: The layout description is stack-owned and references no external memory.
|
|
|
|
|
let layout = unsafe {
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.create_descriptor_set_layout(&layout_info, None)
|
|
|
|
|
}
|
|
|
|
|
.map_err(|result| VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkCreateDescriptorSetLayout",
|
|
|
|
|
result,
|
|
|
|
|
})?;
|
2026-10-11 13:12:50 +04:00
|
|
|
let texture_count = u32::try_from(texture_slot_indices.len()).map_err(|_| {
|
2026-07-18 08:07:45 +04:00
|
|
|
VulkanSmokeRendererError::InvariantViolation {
|
|
|
|
|
context: "static material texture count exceeds Vulkan descriptor limit",
|
|
|
|
|
}
|
|
|
|
|
})?;
|
2026-10-11 13:12:50 +04:00
|
|
|
if texture_count == 0
|
|
|
|
|
|| !texture_slot_indices
|
|
|
|
|
.len()
|
|
|
|
|
.is_multiple_of(TEXTURES_PER_MATERIAL)
|
|
|
|
|
|| texture_slot_indices
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|texture_index| *texture_index >= textures.len())
|
|
|
|
|
{
|
|
|
|
|
// SAFETY: The layout was created above on this device and is rolled
|
|
|
|
|
// back before reporting malformed material-slot input.
|
|
|
|
|
unsafe { device.device().destroy_descriptor_set_layout(layout, None) };
|
2026-07-18 08:07:45 +04:00
|
|
|
return Err(VulkanSmokeRendererError::InvariantViolation {
|
2026-10-11 13:12:50 +04:00
|
|
|
context: "static material texture slots are invalid",
|
2026-07-18 08:07:45 +04:00
|
|
|
});
|
|
|
|
|
}
|
2026-10-11 13:12:50 +04:00
|
|
|
let material_count = texture_count / u32::try_from(TEXTURES_PER_MATERIAL).unwrap_or(1);
|
2026-07-18 07:39:38 +04:00
|
|
|
let pool_size = vk::DescriptorPoolSize::default()
|
|
|
|
|
.ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
2026-07-18 08:07:45 +04:00
|
|
|
.descriptor_count(texture_count);
|
2026-10-11 13:12:50 +04:00
|
|
|
let frame_pool_size = vk::DescriptorPoolSize::default()
|
|
|
|
|
.ty(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
|
|
|
|
|
.descriptor_count(material_count);
|
|
|
|
|
let material_specular_pool_size = vk::DescriptorPoolSize::default()
|
|
|
|
|
.ty(vk::DescriptorType::STORAGE_BUFFER_DYNAMIC)
|
|
|
|
|
.descriptor_count(material_count);
|
|
|
|
|
let pool_sizes = [pool_size, frame_pool_size, material_specular_pool_size];
|
2026-07-18 07:39:38 +04:00
|
|
|
let pool_info = vk::DescriptorPoolCreateInfo::default()
|
|
|
|
|
.pool_sizes(&pool_sizes)
|
2026-10-11 13:12:50 +04:00
|
|
|
.max_sets(material_count);
|
2026-07-18 07:39:38 +04:00
|
|
|
let pool =
|
|
|
|
|
// SAFETY: The pool description is stack-owned and reserves exactly one descriptor.
|
|
|
|
|
unsafe { device.device().create_descriptor_pool(&pool_info, None) }.map_err(|result| {
|
|
|
|
|
// SAFETY: The layout was created above on this device and is rolled back once.
|
|
|
|
|
unsafe { device.device().destroy_descriptor_set_layout(layout, None) };
|
|
|
|
|
VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkCreateDescriptorPool",
|
|
|
|
|
result,
|
|
|
|
|
}
|
|
|
|
|
})?;
|
2026-10-11 13:12:50 +04:00
|
|
|
let layouts = vec![layout; usize::try_from(material_count).unwrap_or(0)];
|
2026-07-18 07:39:38 +04:00
|
|
|
let allocate_info = vk::DescriptorSetAllocateInfo::default()
|
|
|
|
|
.descriptor_pool(pool)
|
|
|
|
|
.set_layouts(&layouts);
|
|
|
|
|
// SAFETY: The pool and layout are live on this logical device.
|
2026-07-18 08:07:45 +04:00
|
|
|
let descriptor_sets = unsafe { device.device().allocate_descriptor_sets(&allocate_info) }
|
2026-07-18 07:39:38 +04:00
|
|
|
.map_err(|result| {
|
|
|
|
|
// SAFETY: Resources were created above on this device and are rolled back once.
|
|
|
|
|
unsafe {
|
|
|
|
|
device.device().destroy_descriptor_pool(pool, None);
|
|
|
|
|
device.device().destroy_descriptor_set_layout(layout, None);
|
|
|
|
|
};
|
|
|
|
|
VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkAllocateDescriptorSets",
|
|
|
|
|
result,
|
|
|
|
|
}
|
2026-07-18 08:07:45 +04:00
|
|
|
})?;
|
2026-07-18 07:39:38 +04:00
|
|
|
let sampler_info = vk::SamplerCreateInfo::default()
|
|
|
|
|
.mag_filter(vk::Filter::LINEAR)
|
|
|
|
|
.min_filter(vk::Filter::LINEAR)
|
|
|
|
|
.mipmap_mode(vk::SamplerMipmapMode::LINEAR)
|
|
|
|
|
.address_mode_u(vk::SamplerAddressMode::REPEAT)
|
|
|
|
|
.address_mode_v(vk::SamplerAddressMode::REPEAT)
|
|
|
|
|
.address_mode_w(vk::SamplerAddressMode::REPEAT)
|
2026-10-11 13:12:50 +04:00
|
|
|
// The uploaded images carry a complete CPU-generated mip chain; the
|
|
|
|
|
// sampler clamps to the largest legal level for each image.
|
|
|
|
|
.max_lod(16.0);
|
2026-07-18 07:39:38 +04:00
|
|
|
let sampler =
|
|
|
|
|
// SAFETY: The sampler create info is stack-owned and has no unsupported optional features.
|
|
|
|
|
unsafe { device.device().create_sampler(&sampler_info, None) }.map_err(|result| {
|
|
|
|
|
// SAFETY: Resources were created above on this device and are rolled back once.
|
|
|
|
|
unsafe {
|
|
|
|
|
device.device().destroy_descriptor_pool(pool, None);
|
|
|
|
|
device.device().destroy_descriptor_set_layout(layout, None);
|
|
|
|
|
};
|
|
|
|
|
VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkCreateSampler",
|
|
|
|
|
result,
|
|
|
|
|
}
|
|
|
|
|
})?;
|
2026-10-11 13:12:50 +04:00
|
|
|
let image_infos = texture_slot_indices
|
2026-07-18 08:07:45 +04:00
|
|
|
.iter()
|
2026-10-11 13:12:50 +04:00
|
|
|
.map(|texture_index| {
|
|
|
|
|
let texture = &textures[*texture_index];
|
2026-07-18 08:07:45 +04:00
|
|
|
vk::DescriptorImageInfo::default()
|
|
|
|
|
.sampler(sampler)
|
|
|
|
|
.image_view(texture.view)
|
|
|
|
|
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>();
|
2026-10-11 13:12:50 +04:00
|
|
|
let frame_info = vk::DescriptorBufferInfo::default()
|
|
|
|
|
.buffer(frame_uniform_buffer.buffer)
|
|
|
|
|
.offset(0)
|
|
|
|
|
.range(VULKAN_FRAME_UNIFORM_BYTES as u64);
|
|
|
|
|
let material_specular_info = vk::DescriptorBufferInfo::default()
|
|
|
|
|
.buffer(material_specular_buffer.buffer)
|
|
|
|
|
.offset(0)
|
|
|
|
|
.range(VULKAN_MATERIAL_SPECULAR_RECORD_BYTES as u64);
|
|
|
|
|
let mut writes = Vec::with_capacity(descriptor_sets.len() * (TEXTURES_PER_MATERIAL + 2));
|
|
|
|
|
for (set_index, descriptor_set) in descriptor_sets.iter().copied().enumerate() {
|
|
|
|
|
for binding in 0..TEXTURES_PER_MATERIAL {
|
|
|
|
|
let image_info = &image_infos[set_index * TEXTURES_PER_MATERIAL + binding];
|
|
|
|
|
writes.push(
|
|
|
|
|
vk::WriteDescriptorSet::default()
|
|
|
|
|
.dst_set(descriptor_set)
|
|
|
|
|
.dst_binding(u32::try_from(binding).unwrap_or(u32::MAX))
|
|
|
|
|
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
|
|
|
|
.image_info(std::slice::from_ref(image_info)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
writes.push(
|
2026-07-18 08:07:45 +04:00
|
|
|
vk::WriteDescriptorSet::default()
|
|
|
|
|
.dst_set(descriptor_set)
|
2026-10-11 13:12:50 +04:00
|
|
|
.dst_binding(FRAME_BINDING)
|
|
|
|
|
.descriptor_type(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
|
|
|
|
|
.buffer_info(std::slice::from_ref(&frame_info)),
|
|
|
|
|
);
|
|
|
|
|
writes.push(
|
|
|
|
|
vk::WriteDescriptorSet::default()
|
|
|
|
|
.dst_set(descriptor_set)
|
|
|
|
|
.dst_binding(MATERIAL_SPECULAR_BINDING)
|
|
|
|
|
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER_DYNAMIC)
|
|
|
|
|
.buffer_info(std::slice::from_ref(&material_specular_info)),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-07-18 08:07:45 +04:00
|
|
|
// SAFETY: Descriptor sets, sampler and image views are live; every texture upload completed its shader-read transition.
|
|
|
|
|
unsafe { device.device().update_descriptor_sets(&writes, &[]) };
|
|
|
|
|
Ok((layout, pool, descriptor_sets, sampler))
|
2026-07-18 07:39:38 +04:00
|
|
|
}
|
|
|
|
|
|
2026-06-25 07:04:22 +04:00
|
|
|
fn extent_component_to_f32(value: u32) -> f32 {
|
|
|
|
|
value as f32
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 08:22:29 +04:00
|
|
|
fn create_graphics_pipeline_cache(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
render_pass: vk::RenderPass,
|
|
|
|
|
pipeline_layout: vk::PipelineLayout,
|
|
|
|
|
extent: (u32, u32),
|
|
|
|
|
draw_ranges: &[super::VulkanStaticDrawRange],
|
|
|
|
|
) -> Result<BTreeMap<PipelineKey, vk::Pipeline>, VulkanSmokeRendererError> {
|
|
|
|
|
let mut pipelines = BTreeMap::new();
|
|
|
|
|
for range in draw_ranges {
|
|
|
|
|
let key = range.pipeline_key();
|
|
|
|
|
if pipelines.contains_key(&key) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let pipeline = match create_graphics_pipeline(
|
|
|
|
|
device,
|
|
|
|
|
render_pass,
|
|
|
|
|
pipeline_layout,
|
|
|
|
|
extent,
|
|
|
|
|
range.pipeline_state,
|
|
|
|
|
) {
|
|
|
|
|
Ok(pipeline) => pipeline,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
// SAFETY: All previously created pipelines belong to this live device and are rolled back with their bundle.
|
|
|
|
|
unsafe {
|
|
|
|
|
for pipeline in pipelines.into_values() {
|
|
|
|
|
device.device().destroy_pipeline(pipeline, None);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
pipelines.insert(key, pipeline);
|
|
|
|
|
}
|
|
|
|
|
Ok(pipelines)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 07:04:22 +04:00
|
|
|
fn create_graphics_pipeline(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
render_pass: vk::RenderPass,
|
|
|
|
|
pipeline_layout: vk::PipelineLayout,
|
|
|
|
|
extent: (u32, u32),
|
2026-07-18 08:22:29 +04:00
|
|
|
state: LegacyPipelineState,
|
2026-06-25 07:04:22 +04:00
|
|
|
) -> Result<vk::Pipeline, VulkanSmokeRendererError> {
|
2026-10-11 13:12:50 +04:00
|
|
|
let vertex_shader = create_shader_module(device, TRIANGLE_VERTEX_SHADER_WORDS, "vertex")?;
|
|
|
|
|
let fragment_shader =
|
|
|
|
|
match create_shader_module(device, TRIANGLE_FRAGMENT_SHADER_WORDS, "fragment") {
|
|
|
|
|
Ok(module) => module,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
// SAFETY: The shader module was created above on this live logical device and is destroyed on setup failure.
|
|
|
|
|
unsafe { device.device().destroy_shader_module(vertex_shader, None) };
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-06-25 07:04:22 +04:00
|
|
|
let entry_point = c"main";
|
|
|
|
|
let shader_stages = [
|
|
|
|
|
vk::PipelineShaderStageCreateInfo::default()
|
|
|
|
|
.module(vertex_shader)
|
|
|
|
|
.name(entry_point)
|
|
|
|
|
.stage(vk::ShaderStageFlags::VERTEX),
|
|
|
|
|
vk::PipelineShaderStageCreateInfo::default()
|
|
|
|
|
.module(fragment_shader)
|
|
|
|
|
.name(entry_point)
|
|
|
|
|
.stage(vk::ShaderStageFlags::FRAGMENT),
|
|
|
|
|
];
|
|
|
|
|
let vertex_binding = vk::VertexInputBindingDescription::default()
|
|
|
|
|
.binding(0)
|
2026-10-11 13:12:50 +04:00
|
|
|
.stride(u32::try_from(14 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX))
|
2026-06-25 07:04:22 +04:00
|
|
|
.input_rate(vk::VertexInputRate::VERTEX);
|
|
|
|
|
let vertex_attributes = [
|
|
|
|
|
vk::VertexInputAttributeDescription::default()
|
|
|
|
|
.binding(0)
|
|
|
|
|
.location(0)
|
2026-07-18 14:53:11 +04:00
|
|
|
.format(vk::Format::R32G32B32_SFLOAT)
|
2026-06-25 07:04:22 +04:00
|
|
|
.offset(0),
|
|
|
|
|
vk::VertexInputAttributeDescription::default()
|
|
|
|
|
.binding(0)
|
|
|
|
|
.location(1)
|
|
|
|
|
.format(vk::Format::R32G32B32_SFLOAT)
|
2026-07-18 14:53:11 +04:00
|
|
|
.offset(u32::try_from(3 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX)),
|
2026-07-18 07:39:38 +04:00
|
|
|
vk::VertexInputAttributeDescription::default()
|
|
|
|
|
.binding(0)
|
|
|
|
|
.location(2)
|
2026-10-11 13:12:50 +04:00
|
|
|
.format(vk::Format::R32G32B32_SFLOAT)
|
2026-07-18 14:53:11 +04:00
|
|
|
.offset(u32::try_from(6 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX)),
|
2026-10-11 13:12:50 +04:00
|
|
|
vk::VertexInputAttributeDescription::default()
|
|
|
|
|
.binding(0)
|
|
|
|
|
.location(3)
|
|
|
|
|
.format(vk::Format::R32G32_SFLOAT)
|
|
|
|
|
.offset(u32::try_from(9 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX)),
|
|
|
|
|
vk::VertexInputAttributeDescription::default()
|
|
|
|
|
.binding(0)
|
|
|
|
|
.location(4)
|
|
|
|
|
.format(vk::Format::R32G32_SFLOAT)
|
|
|
|
|
.offset(u32::try_from(11 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX)),
|
|
|
|
|
vk::VertexInputAttributeDescription::default()
|
|
|
|
|
.binding(0)
|
|
|
|
|
.location(5)
|
|
|
|
|
.format(vk::Format::R32_SFLOAT)
|
|
|
|
|
.offset(u32::try_from(13 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX)),
|
2026-06-25 07:04:22 +04:00
|
|
|
];
|
|
|
|
|
let vertex_bindings = [vertex_binding];
|
|
|
|
|
let vertex_input_state = vk::PipelineVertexInputStateCreateInfo::default()
|
|
|
|
|
.vertex_binding_descriptions(&vertex_bindings)
|
|
|
|
|
.vertex_attribute_descriptions(&vertex_attributes);
|
|
|
|
|
let input_assembly_state = vk::PipelineInputAssemblyStateCreateInfo::default()
|
|
|
|
|
.topology(vk::PrimitiveTopology::TRIANGLE_LIST)
|
|
|
|
|
.primitive_restart_enable(false);
|
|
|
|
|
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()
|
|
|
|
|
.depth_clamp_enable(false)
|
|
|
|
|
.rasterizer_discard_enable(false)
|
|
|
|
|
.polygon_mode(vk::PolygonMode::FILL)
|
|
|
|
|
.line_width(1.0)
|
2026-07-18 08:22:29 +04:00
|
|
|
.cull_mode(match state.cull {
|
|
|
|
|
LegacyCullMode::Disabled => vk::CullModeFlags::NONE,
|
|
|
|
|
LegacyCullMode::BackFace => vk::CullModeFlags::BACK,
|
|
|
|
|
LegacyCullMode::FrontFace => vk::CullModeFlags::FRONT,
|
|
|
|
|
})
|
2026-06-25 07:04:22 +04:00
|
|
|
.front_face(vk::FrontFace::CLOCKWISE)
|
|
|
|
|
.depth_bias_enable(false);
|
|
|
|
|
let multisample_state = vk::PipelineMultisampleStateCreateInfo::default()
|
|
|
|
|
.sample_shading_enable(false)
|
|
|
|
|
.rasterization_samples(vk::SampleCountFlags::TYPE_1);
|
2026-07-18 08:31:19 +04:00
|
|
|
let depth_stencil_state = vk::PipelineDepthStencilStateCreateInfo::default()
|
|
|
|
|
.depth_test_enable(state.depth != LegacyDepthMode::Disabled)
|
|
|
|
|
.depth_write_enable(state.depth == LegacyDepthMode::TestWrite)
|
|
|
|
|
.depth_compare_op(vk::CompareOp::LESS_OR_EQUAL)
|
|
|
|
|
.depth_bounds_test_enable(false)
|
|
|
|
|
.stencil_test_enable(false);
|
2026-10-11 13:12:50 +04:00
|
|
|
let (blend_enable, src_color_blend_factor, dst_color_blend_factor) = match state.blend {
|
|
|
|
|
LegacyBlendMode::Opaque => (false, vk::BlendFactor::ONE, vk::BlendFactor::ZERO),
|
|
|
|
|
LegacyBlendMode::SourceAlpha => (
|
|
|
|
|
true,
|
|
|
|
|
vk::BlendFactor::SRC_ALPHA,
|
|
|
|
|
vk::BlendFactor::ONE_MINUS_SRC_ALPHA,
|
|
|
|
|
),
|
|
|
|
|
LegacyBlendMode::Additive => (true, vk::BlendFactor::SRC_ALPHA, vk::BlendFactor::ONE),
|
|
|
|
|
LegacyBlendMode::ZeroSourceColor => {
|
|
|
|
|
(true, vk::BlendFactor::ZERO, vk::BlendFactor::SRC_COLOR)
|
|
|
|
|
}
|
|
|
|
|
LegacyBlendMode::DestColorSourceColor => {
|
|
|
|
|
(true, vk::BlendFactor::DST_COLOR, vk::BlendFactor::SRC_COLOR)
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-06-25 07:04:22 +04:00
|
|
|
let color_blend_attachment = vk::PipelineColorBlendAttachmentState::default()
|
|
|
|
|
.color_write_mask(
|
|
|
|
|
vk::ColorComponentFlags::R
|
|
|
|
|
| vk::ColorComponentFlags::G
|
|
|
|
|
| vk::ColorComponentFlags::B
|
|
|
|
|
| vk::ColorComponentFlags::A,
|
|
|
|
|
)
|
2026-10-11 13:12:50 +04:00
|
|
|
.blend_enable(blend_enable)
|
|
|
|
|
.src_color_blend_factor(src_color_blend_factor)
|
|
|
|
|
.dst_color_blend_factor(dst_color_blend_factor)
|
2026-07-18 08:22:29 +04:00
|
|
|
.color_blend_op(vk::BlendOp::ADD)
|
2026-10-11 13:12:50 +04:00
|
|
|
.src_alpha_blend_factor(src_color_blend_factor)
|
|
|
|
|
.dst_alpha_blend_factor(dst_color_blend_factor)
|
2026-07-18 08:22:29 +04:00
|
|
|
.alpha_blend_op(vk::BlendOp::ADD);
|
2026-06-25 07:04:22 +04:00
|
|
|
let color_blend_attachments = [color_blend_attachment];
|
|
|
|
|
let color_blend_state = vk::PipelineColorBlendStateCreateInfo::default()
|
|
|
|
|
.logic_op_enable(false)
|
|
|
|
|
.attachments(&color_blend_attachments);
|
|
|
|
|
let create_info = vk::GraphicsPipelineCreateInfo::default()
|
|
|
|
|
.stages(&shader_stages)
|
|
|
|
|
.vertex_input_state(&vertex_input_state)
|
|
|
|
|
.input_assembly_state(&input_assembly_state)
|
|
|
|
|
.viewport_state(&viewport_state)
|
|
|
|
|
.rasterization_state(&rasterization_state)
|
|
|
|
|
.multisample_state(&multisample_state)
|
2026-07-18 08:31:19 +04:00
|
|
|
.depth_stencil_state(&depth_stencil_state)
|
2026-06-25 07:04:22 +04:00
|
|
|
.color_blend_state(&color_blend_state)
|
|
|
|
|
.layout(pipeline_layout)
|
|
|
|
|
.render_pass(render_pass)
|
|
|
|
|
.subpass(0);
|
|
|
|
|
let create_infos = [create_info];
|
|
|
|
|
// SAFETY: Pipeline creation references only stack-owned descriptions and live render-pass/layout handles.
|
|
|
|
|
let pipeline_result = unsafe {
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.create_graphics_pipelines(vk::PipelineCache::null(), &create_infos, None)
|
|
|
|
|
};
|
|
|
|
|
// SAFETY: The shader modules were created above on this live logical device and are no longer needed after pipeline creation.
|
|
|
|
|
unsafe {
|
|
|
|
|
device.device().destroy_shader_module(vertex_shader, None);
|
|
|
|
|
device.device().destroy_shader_module(fragment_shader, None);
|
|
|
|
|
}
|
|
|
|
|
let pipelines =
|
|
|
|
|
pipeline_result.map_err(|(_, error)| VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkCreateGraphicsPipelines",
|
|
|
|
|
result: error,
|
|
|
|
|
})?;
|
|
|
|
|
Ok(pipelines[0])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_shader_module(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
words: &[u32],
|
2026-10-11 13:12:50 +04:00
|
|
|
stage: &'static str,
|
2026-06-25 07:04:22 +04:00
|
|
|
) -> Result<vk::ShaderModule, VulkanSmokeRendererError> {
|
|
|
|
|
let create_info = vk::ShaderModuleCreateInfo::default().code(words);
|
|
|
|
|
// SAFETY: The SPIR-V slice points to static checked-in words and lives for the duration of the call.
|
|
|
|
|
unsafe { device.device().create_shader_module(&create_info, None) }.map_err(|error| {
|
|
|
|
|
VulkanSmokeRendererError::VulkanOperation {
|
2026-10-11 13:12:50 +04:00
|
|
|
context: if stage == "vertex" {
|
|
|
|
|
"vkCreateShaderModule(vertex)"
|
|
|
|
|
} else {
|
|
|
|
|
"vkCreateShaderModule(fragment)"
|
|
|
|
|
},
|
2026-06-25 07:04:22 +04:00
|
|
|
result: error,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_framebuffer(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
render_pass: vk::RenderPass,
|
|
|
|
|
image_view: vk::ImageView,
|
2026-07-18 08:31:19 +04:00
|
|
|
depth_view: vk::ImageView,
|
2026-06-25 07:04:22 +04:00
|
|
|
extent: (u32, u32),
|
|
|
|
|
) -> Result<vk::Framebuffer, VulkanSmokeRendererError> {
|
2026-07-18 08:31:19 +04:00
|
|
|
let attachments = [image_view, depth_view];
|
2026-06-25 07:04:22 +04:00
|
|
|
let create_info = vk::FramebufferCreateInfo::default()
|
|
|
|
|
.render_pass(render_pass)
|
|
|
|
|
.attachments(&attachments)
|
|
|
|
|
.width(extent.0)
|
|
|
|
|
.height(extent.1)
|
|
|
|
|
.layers(1);
|
|
|
|
|
// SAFETY: The framebuffer references a live image view and render pass owned by this logical device.
|
|
|
|
|
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: The command pool belongs to this live logical device and the allocation info is stack-owned.
|
|
|
|
|
unsafe { device.device().allocate_command_buffers(&allocate_info) }.map_err(|error| {
|
|
|
|
|
VulkanSmokeRendererError::VulkanOperation {
|
|
|
|
|
context: "vkAllocateCommandBuffers",
|
|
|
|
|
result: error,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn destroy_swapchain_resources(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
command_pool: vk::CommandPool,
|
|
|
|
|
resources: VulkanSwapchainResources,
|
|
|
|
|
) {
|
|
|
|
|
// SAFETY: All handles belong to this live logical device and are destroyed during renderer teardown/recreation.
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-07-18 08:22:29 +04:00
|
|
|
for pipeline in resources.pipelines.into_values() {
|
|
|
|
|
device.device().destroy_pipeline(pipeline, None);
|
|
|
|
|
}
|
2026-06-25 07:04:22 +04:00
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_pipeline_layout(resources.pipeline_layout, None);
|
2026-07-18 07:39:38 +04:00
|
|
|
device.device().destroy_sampler(resources.sampler, None);
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_descriptor_pool(resources.descriptor_pool, None);
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_descriptor_set_layout(resources.descriptor_set_layout, None);
|
2026-06-25 07:04:22 +04:00
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_render_pass(resources.render_pass, None);
|
2026-10-11 13:12:50 +04:00
|
|
|
for depth_attachment in resources.depth_attachments {
|
|
|
|
|
destroy_depth_attachment(device, &depth_attachment);
|
|
|
|
|
}
|
2026-06-25 07:04:22 +04:00
|
|
|
for image_view in resources.image_views {
|
|
|
|
|
device.device().destroy_image_view(image_view, None);
|
|
|
|
|
}
|
2026-07-18 09:05:20 +04:00
|
|
|
for buffer in resources.readback_buffers {
|
|
|
|
|
device.device().destroy_buffer(buffer.buffer, None);
|
|
|
|
|
device.device().free_memory(buffer.memory, None);
|
|
|
|
|
}
|
2026-10-11 13:12:50 +04:00
|
|
|
for semaphore in resources.render_finished {
|
|
|
|
|
device.device().destroy_semaphore(semaphore, None);
|
|
|
|
|
}
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_buffer(resources.frame_uniform_buffer.buffer, None);
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.free_memory(resources.frame_uniform_buffer.memory, None);
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_buffer(resources.material_specular_buffer.buffer, None);
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.free_memory(resources.material_specular_buffer.memory, None);
|
2026-06-25 07:04:22 +04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn destroy_partial_swapchain_resources(
|
|
|
|
|
device: &VulkanLogicalDeviceProbe,
|
|
|
|
|
command_pool: vk::CommandPool,
|
|
|
|
|
partial: PartialSwapchainResources,
|
|
|
|
|
) {
|
|
|
|
|
// SAFETY: All handles in the partial bundle belong to this live logical device and are destroyed on setup failure.
|
|
|
|
|
unsafe {
|
|
|
|
|
if !partial.command_buffers.is_empty() {
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.free_command_buffers(command_pool, &partial.command_buffers);
|
|
|
|
|
}
|
|
|
|
|
for framebuffer in partial.framebuffers {
|
|
|
|
|
device.device().destroy_framebuffer(framebuffer, None);
|
|
|
|
|
}
|
2026-07-18 08:22:29 +04:00
|
|
|
for pipeline in partial.pipelines.into_values() {
|
2026-06-25 07:04:22 +04:00
|
|
|
device.device().destroy_pipeline(pipeline, None);
|
|
|
|
|
}
|
|
|
|
|
if let Some(pipeline_layout) = partial.pipeline_layout {
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_pipeline_layout(pipeline_layout, None);
|
|
|
|
|
}
|
2026-07-18 07:39:38 +04:00
|
|
|
if let Some(sampler) = partial.sampler {
|
|
|
|
|
device.device().destroy_sampler(sampler, None);
|
|
|
|
|
}
|
|
|
|
|
if let Some(descriptor_pool) = partial.descriptor_pool {
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_descriptor_pool(descriptor_pool, None);
|
|
|
|
|
}
|
|
|
|
|
if let Some(descriptor_set_layout) = partial.descriptor_set_layout {
|
|
|
|
|
device
|
|
|
|
|
.device()
|
|
|
|
|
.destroy_descriptor_set_layout(descriptor_set_layout, None);
|
|
|
|
|
}
|
2026-06-25 07:04:22 +04:00
|
|
|
if let Some(render_pass) = partial.render_pass {
|
|
|
|
|
device.device().destroy_render_pass(render_pass, None);
|
|
|
|
|
}
|
2026-10-11 13:12:50 +04:00
|
|
|
for depth_attachment in partial.depth_attachments {
|
2026-07-18 08:31:19 +04:00
|
|
|
destroy_depth_attachment(device, &depth_attachment);
|
|
|
|
|
}
|
2026-06-25 07:04:22 +04:00
|
|
|
for image_view in partial.image_views {
|
|
|
|
|
device.device().destroy_image_view(image_view, None);
|
|
|
|
|
}
|
2026-07-18 09:05:20 +04:00
|
|
|
for buffer in partial.readback_buffers {
|
|
|
|
|
device.device().destroy_buffer(buffer.buffer, None);
|
|
|
|
|
device.device().free_memory(buffer.memory, None);
|
|
|
|
|
}
|
2026-10-11 13:12:50 +04:00
|
|
|
for semaphore in partial.render_finished {
|
|
|
|
|
device.device().destroy_semaphore(semaphore, None);
|
|
|
|
|
}
|
|
|
|
|
if let Some(buffer) = partial.frame_uniform_buffer {
|
|
|
|
|
device.device().destroy_buffer(buffer.buffer, None);
|
|
|
|
|
device.device().free_memory(buffer.memory, None);
|
|
|
|
|
}
|
|
|
|
|
if let Some(buffer) = partial.material_specular_buffer {
|
|
|
|
|
device.device().destroy_buffer(buffer.buffer, None);
|
|
|
|
|
device.device().free_memory(buffer.memory, None);
|
|
|
|
|
}
|
2026-06-25 07:04:22 +04:00
|
|
|
}
|
|
|
|
|
}
|