use std::sync::{Arc, Mutex};
use ash::vk;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Retired {
Pipeline(vk::Pipeline),
PipelineLayout(vk::PipelineLayout),
SetLayout(vk::DescriptorSetLayout),
DescriptorPool(vk::DescriptorPool),
RenderPass(vk::RenderPass),
Framebuffer(vk::Framebuffer),
Sampler(vk::Sampler),
}
impl Retired {
unsafe fn destroy(self, device: &ash::Device) {
unsafe {
match self {
Retired::Pipeline(h) => device.destroy_pipeline(h, None),
Retired::PipelineLayout(h) => device.destroy_pipeline_layout(h, None),
Retired::SetLayout(h) => device.destroy_descriptor_set_layout(h, None),
Retired::DescriptorPool(h) => device.destroy_descriptor_pool(h, None),
Retired::RenderPass(h) => device.destroy_render_pass(h, None),
Retired::Framebuffer(h) => device.destroy_framebuffer(h, None),
Retired::Sampler(h) => device.destroy_sampler(h, None),
}
}
}
}
#[derive(Clone, Copy)]
struct Pending {
handle: Retired,
retire_at: u64,
}
struct RetireQueue {
pending: Vec<Pending>,
frame: u64,
depth: u64,
}
impl RetireQueue {
fn new(frames_in_flight: usize) -> Self {
Self {
pending: Vec::new(),
frame: 0,
depth: frames_in_flight as u64 + 1,
}
}
fn push(&mut self, handle: Retired) {
self.pending.push(Pending {
handle,
retire_at: self.frame + self.depth,
});
}
fn tick(&mut self) -> Vec<Retired> {
self.frame += 1;
let frame = self.frame;
let mut due = Vec::new();
self.pending.retain(|p| {
if p.retire_at <= frame {
due.push(p.handle);
false
} else {
true
}
});
due
}
fn drain(&mut self) -> Vec<Retired> {
std::mem::take(&mut self.pending)
.into_iter()
.map(|p| p.handle)
.collect()
}
#[cfg(test)]
fn len(&self) -> usize {
self.pending.len()
}
}
struct DeviceInner {
raw: ash::Device,
instance: ash::Instance,
_entry: ash::Entry,
queue: Mutex<RetireQueue>,
debug: DebugMessenger,
}
pub(in crate::vulkan) struct DebugMessenger {
pub(in crate::vulkan) utils: Option<ash::ext::debug_utils::Instance>,
pub(in crate::vulkan) messenger: Option<vk::DebugUtilsMessengerEXT>,
pub(in crate::vulkan) filter: Option<Box<std::sync::atomic::AtomicU32>>,
}
impl DeviceInner {
fn queue(&self) -> std::sync::MutexGuard<'_, RetireQueue> {
self.queue.lock().unwrap_or_else(|e| e.into_inner())
}
}
impl Drop for DeviceInner {
fn drop(&mut self) {
let due = self.queue().drain();
for handle in due {
unsafe { handle.destroy(&self.raw) };
}
super::pipeline_cache::shutdown(&self.raw);
unsafe { self.raw.destroy_device(None) };
if let (Some(du), Some(dm)) = (&self.debug.utils, self.debug.messenger) {
unsafe { du.destroy_debug_utils_messenger(dm, None) };
}
unsafe { self.instance.destroy_instance(None) };
}
}
#[derive(Clone)]
pub(in crate::vulkan) struct VkDevice {
inner: Arc<DeviceInner>,
}
const _: () = {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<VkDevice>();
};
impl std::ops::Deref for VkDevice {
type Target = ash::Device;
fn deref(&self) -> &ash::Device {
&self.inner.raw
}
}
impl VkDevice {
pub(in crate::vulkan) fn new(
entry: ash::Entry,
instance: ash::Instance,
raw: ash::Device,
frames_in_flight: usize,
debug: DebugMessenger,
) -> Self {
Self {
inner: Arc::new(DeviceInner {
raw,
instance,
_entry: entry,
queue: Mutex::new(RetireQueue::new(frames_in_flight)),
debug,
}),
}
}
pub(in crate::vulkan) fn debug_filter(&self) -> Option<&std::sync::atomic::AtomicU32> {
self.inner.debug.filter.as_deref()
}
pub(in crate::vulkan) fn begin_frame(&self) {
let due = self.inner.queue().tick();
self.destroy_all(due);
}
pub(in crate::vulkan) fn reclaim_idle(&self) {
let due = self.inner.queue().drain();
self.destroy_all(due);
}
fn destroy_all(&self, due: Vec<Retired>) {
for handle in due {
unsafe { handle.destroy(&self.inner.raw) };
}
}
fn retire(&self, handle: Retired) {
self.inner.queue().push(handle);
}
}
macro_rules! create_owned {
($name:ident, $info:ty, $owned:ident) => {
impl VkDevice {
pub(in crate::vulkan) fn $name(&self, info: &$info) -> Result<$owned, vk::Result> {
let handle = unsafe { self.inner.raw.$name(info, None) }?;
Ok($owned::new(self, handle))
}
}
};
}
create_owned!(
create_pipeline_layout,
vk::PipelineLayoutCreateInfo<'_>,
OwnedPipelineLayout
);
create_owned!(
create_descriptor_set_layout,
vk::DescriptorSetLayoutCreateInfo<'_>,
OwnedSetLayout
);
create_owned!(
create_descriptor_pool,
vk::DescriptorPoolCreateInfo<'_>,
OwnedDescriptorPool
);
create_owned!(
create_render_pass,
vk::RenderPassCreateInfo<'_>,
OwnedRenderPass
);
create_owned!(
create_framebuffer,
vk::FramebufferCreateInfo<'_>,
OwnedFramebuffer
);
create_owned!(create_sampler, vk::SamplerCreateInfo<'_>, OwnedSampler);
macro_rules! owned_handle {
($name:ident, $vk:ty, $kind:ident) => {
pub(in crate::vulkan) struct $name {
handle: $vk,
device: Option<VkDevice>,
}
impl $name {
pub(in crate::vulkan) fn new(device: &VkDevice, handle: $vk) -> Self {
Self {
handle,
device: Some(device.clone()),
}
}
pub(in crate::vulkan) fn handle(&self) -> $vk {
self.handle
}
pub(in crate::vulkan) fn null() -> Self {
Self {
handle: <$vk>::null(),
device: None,
}
}
}
impl Default for $name {
fn default() -> Self {
Self::null()
}
}
impl Drop for $name {
fn drop(&mut self) {
if let Some(device) = &self.device {
device.retire(Retired::$kind(self.handle));
}
}
}
impl std::fmt::Debug for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple(stringify!($name))
.field(&self.handle)
.finish()
}
}
};
}
owned_handle!(OwnedPipeline, vk::Pipeline, Pipeline);
owned_handle!(OwnedPipelineLayout, vk::PipelineLayout, PipelineLayout);
owned_handle!(OwnedSetLayout, vk::DescriptorSetLayout, SetLayout);
owned_handle!(OwnedDescriptorPool, vk::DescriptorPool, DescriptorPool);
owned_handle!(OwnedRenderPass, vk::RenderPass, RenderPass);
owned_handle!(OwnedFramebuffer, vk::Framebuffer, Framebuffer);
owned_handle!(OwnedSampler, vk::Sampler, Sampler);
impl OwnedFramebuffer {
pub(in crate::vulkan) fn is_null(&self) -> bool {
self.device.is_none()
}
}
#[cfg(test)]
mod tests {
use ash::vk::Handle as _;
use super::*;
fn pipeline(raw: u64) -> Retired {
Retired::Pipeline(vk::Pipeline::from_raw(raw))
}
#[test]
fn a_handle_is_withheld_for_frames_in_flight_plus_one_ticks() {
let mut queue = RetireQueue::new(2);
queue.push(pipeline(1));
assert!(queue.tick().is_empty());
assert!(queue.tick().is_empty());
assert_eq!(queue.tick(), vec![pipeline(1)]);
assert_eq!(queue.len(), 0);
}
#[test]
fn handles_queued_on_different_frames_retire_on_different_ticks() {
let mut queue = RetireQueue::new(1);
queue.push(pipeline(1));
assert!(queue.tick().is_empty());
queue.push(pipeline(2));
assert_eq!(queue.tick(), vec![pipeline(1)]);
assert_eq!(queue.tick(), vec![pipeline(2)]);
}
#[test]
fn a_single_tick_returns_every_handle_whose_window_closed() {
let mut queue = RetireQueue::new(0);
queue.push(pipeline(1));
queue.push(pipeline(2));
assert_eq!(queue.tick(), vec![pipeline(1), pipeline(2)]);
assert_eq!(queue.len(), 0);
}
#[test]
fn draining_ignores_the_window() {
let mut queue = RetireQueue::new(8);
queue.push(pipeline(1));
queue.push(pipeline(2));
assert_eq!(queue.drain(), vec![pipeline(1), pipeline(2)]);
assert_eq!(queue.len(), 0);
assert!(queue.drain().is_empty());
}
#[test]
fn each_kind_carries_its_own_handle() {
let entries = [
Retired::Pipeline(vk::Pipeline::from_raw(1)),
Retired::PipelineLayout(vk::PipelineLayout::from_raw(1)),
Retired::SetLayout(vk::DescriptorSetLayout::from_raw(1)),
Retired::DescriptorPool(vk::DescriptorPool::from_raw(1)),
Retired::RenderPass(vk::RenderPass::from_raw(1)),
Retired::Framebuffer(vk::Framebuffer::from_raw(1)),
Retired::Sampler(vk::Sampler::from_raw(1)),
];
for (i, a) in entries.iter().enumerate() {
for (j, b) in entries.iter().enumerate() {
assert_eq!(i == j, a == b, "{a:?} vs {b:?}");
}
}
}
#[test]
fn a_null_wrapper_owns_nothing() {
let p = OwnedPipeline::null();
assert_eq!(p.handle(), vk::Pipeline::null());
drop(p);
assert!(OwnedFramebuffer::default().is_null());
}
}