Files
fparkan/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
T

780 lines
29 KiB
Rust
Raw Normal View History

#![allow(unsafe_code)]
use ash::vk;
use super::{
create_command_pool, create_frame_sync, create_swapchain_resources,
create_triangle_index_buffer, create_triangle_vertex_buffer, create_validation_messenger,
create_vulkan_instance_probe, create_vulkan_logical_device_probe, create_vulkan_surface_probe,
create_vulkan_swapchain_probe_for_extent, destroy_allocated_buffer,
destroy_swapchain_resources, plan_vulkan_surface, VulkanAllocatedBuffer, VulkanInstanceConfig,
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeFrameOutcome, VulkanSmokeRenderer,
VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError, VulkanSmokeRendererReport,
VulkanSurfaceProbe, VulkanSwapchainProbe, VulkanSwapchainResources, VulkanValidationMessenger,
VulkanValidationReport,
};
use crate::shader_manifest::{triangle_shader_manifest, validate_shader_manifest};
fn take_runtime_owners_in_dependency_order<Instance, Validation, Surface, Device, Swapchain>(
instance: &mut Option<Instance>,
validation: &mut Option<Validation>,
surface: &mut Option<Surface>,
device: &mut Option<Device>,
swapchain: &mut Option<Swapchain>,
) {
swapchain.take();
device.take();
surface.take();
validation.take();
instance.take();
}
struct RollbackOnDrop<T, F>
where
F: FnOnce(T),
{
value: Option<T>,
rollback: Option<F>,
}
impl<T, F> RollbackOnDrop<T, F>
where
F: FnOnce(T),
{
fn new(value: T, rollback: F) -> Self {
Self {
value: Some(value),
rollback: Some(rollback),
}
}
fn commit(mut self) -> T {
self.rollback.take();
self.value
.take()
.expect("rollback guard must hold a value until commit")
}
}
impl<T, F> Drop for RollbackOnDrop<T, F>
where
F: FnOnce(T),
{
fn drop(&mut self) {
if let (Some(value), Some(rollback)) = (self.value.take(), self.rollback.take()) {
rollback(value);
}
}
}
impl VulkanSmokeRenderer {
/// Creates a live Vulkan smoke renderer bound to a live native window.
///
/// # Errors
///
/// Returns [`VulkanSmokeRendererError`] when Vulkan bootstrap, pipeline creation,
/// memory allocation, or synchronization resource creation fails.
pub fn new(
create_info: &VulkanSmokeRendererCreateInfo,
) -> Result<Self, VulkanSmokeRendererError> {
let shader_manifest = validate_shader_manifest(&triangle_shader_manifest())
.map_err(VulkanSmokeRendererError::ShaderManifest)?;
let surface_plan = plan_vulkan_surface(Some(create_info.native_handles))
.map_err(VulkanSmokeRendererError::Surface)?;
let mut instance_config = VulkanInstanceConfig::smoke(&create_info.application_name);
instance_config
.required_extensions
.clone_from(&surface_plan.required_instance_extensions);
instance_config.enable_validation = create_info.enable_validation;
let instance = create_vulkan_instance_probe(&instance_config)
.map_err(VulkanSmokeRendererError::Instance)?;
let validation = if create_info.enable_validation {
Some(create_validation_messenger(&instance)?)
} else {
None
};
let surface = create_vulkan_surface_probe(&instance, Some(create_info.native_handles))
.map_err(VulkanSmokeRendererError::Surface)?;
let device =
create_vulkan_logical_device_probe(&instance, &surface, create_info.drawable_extent)
.map_err(VulkanSmokeRendererError::LogicalDevice)?;
let swapchain = create_vulkan_swapchain_probe_for_extent(
&instance,
&surface,
&device,
create_info.drawable_extent,
vk::SwapchainKHR::null(),
)
.map_err(VulkanSmokeRendererError::Swapchain)?;
let command_pool = create_command_pool(&device)?;
let vertex_buffer = match create_triangle_vertex_buffer(&instance, &device) {
Ok(buffer) => buffer,
Err(error) => {
// SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
unsafe { device.device().destroy_command_pool(command_pool, None) };
return Err(error);
}
};
let index_buffer = match create_triangle_index_buffer(&instance, &device) {
Ok(buffer) => buffer,
Err(error) => {
// SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
unsafe { device.device().destroy_command_pool(command_pool, None) };
destroy_allocated_buffer(&device, &vertex_buffer);
return Err(error);
}
};
let mut renderer = Self {
instance: Some(instance),
validation,
surface: Some(surface),
device: Some(device),
swapchain: Some(swapchain),
command_pool,
swapchain_resources: None,
vertex_buffer: Some(vertex_buffer),
index_buffer: Some(index_buffer),
frame_sync: Vec::new(),
images_in_flight: Vec::new(),
current_frame: 0,
pending_extent: None,
swapchain_recreate_count: 0,
report: VulkanSmokeRendererReport {
shader_manifest_hash: shader_manifest.manifest_hash.clone(),
portability_enumeration: instance_config.enable_portability_enumeration,
device_name: String::new(),
graphics_queue_family: 0,
present_queue_family: 0,
enabled_extension_count: 0,
swapchain_extent: (0, 0),
swapchain_image_count: 0,
},
};
renderer.rebuild_swapchain_resources(false)?;
let device_ref = renderer.device_ref()?;
let swapchain_ref = renderer.swapchain_ref()?;
renderer.report = VulkanSmokeRendererReport {
shader_manifest_hash: shader_manifest.manifest_hash,
portability_enumeration: renderer
.instance
.as_ref()
.is_some_and(|instance| instance.report.create_flags != 0),
device_name: device_ref.report.device_name.clone(),
graphics_queue_family: device_ref.report.graphics_queue_family,
present_queue_family: device_ref.report.present_queue_family,
enabled_extension_count: device_ref
.report
.enabled_extensions
.len()
.try_into()
.unwrap_or(u32::MAX),
swapchain_extent: swapchain_ref.report.plan.extent,
swapchain_image_count: swapchain_ref.report.image_count,
};
Ok(renderer)
}
/// Returns the current bootstrap report.
#[must_use]
pub const fn report(&self) -> &VulkanSmokeRendererReport {
&self.report
}
/// Returns measured validation counters and VUIDs.
#[must_use]
pub fn validation_report(&self) -> VulkanValidationReport {
self.validation.as_ref().map_or(
VulkanValidationReport {
warning_count: 0,
error_count: 0,
vuids: Vec::new(),
},
VulkanValidationMessenger::report,
)
}
/// Returns the measured swapchain recreation count.
#[must_use]
pub const fn swapchain_recreate_count(&self) -> u32 {
self.swapchain_recreate_count
}
/// Requests swapchain recreation for a new drawable extent.
pub fn request_resize(&mut self, extent: (u32, u32)) {
self.pending_extent = Some(extent);
}
fn device_ref(&self) -> Result<&VulkanLogicalDeviceProbe, VulkanSmokeRendererError> {
self.device
.as_ref()
.ok_or(VulkanSmokeRendererError::InvariantViolation {
context: "logical device",
})
}
fn swapchain_ref(&self) -> Result<&VulkanSwapchainProbe, VulkanSmokeRendererError> {
self.swapchain
.as_ref()
.ok_or(VulkanSmokeRendererError::InvariantViolation {
context: "swapchain",
})
}
fn instance_ref(&self) -> Result<&VulkanInstanceProbe, VulkanSmokeRendererError> {
self.instance
.as_ref()
.ok_or(VulkanSmokeRendererError::InvariantViolation {
context: "instance",
})
}
fn surface_ref(&self) -> Result<&VulkanSurfaceProbe, VulkanSmokeRendererError> {
self.surface
.as_ref()
.ok_or(VulkanSmokeRendererError::InvariantViolation { context: "surface" })
}
fn resources_ref(&self) -> Result<&VulkanSwapchainResources, VulkanSmokeRendererError> {
self.swapchain_resources
.as_ref()
.ok_or(VulkanSmokeRendererError::InvariantViolation {
context: "swapchain resources",
})
}
fn vertex_buffer_ref(&self) -> Result<&VulkanAllocatedBuffer, VulkanSmokeRendererError> {
self.vertex_buffer
.as_ref()
.ok_or(VulkanSmokeRendererError::InvariantViolation {
context: "vertex buffer",
})
}
fn index_buffer_ref(&self) -> Result<&VulkanAllocatedBuffer, VulkanSmokeRendererError> {
self.index_buffer
.as_ref()
.ok_or(VulkanSmokeRendererError::InvariantViolation {
context: "index buffer",
})
}
/// Draws and presents one indexed-triangle frame.
///
/// # Errors
///
/// Returns [`VulkanSmokeRendererError`] when synchronization, command recording,
/// submission, or presentation fails.
#[allow(clippy::too_many_lines)]
pub fn draw_frame(&mut self) -> Result<VulkanSmokeFrameOutcome, VulkanSmokeRendererError> {
if let Some(extent) = self.pending_extent.take() {
if extent.0 == 0 || extent.1 == 0 {
self.pending_extent = Some(extent);
return Ok(VulkanSmokeFrameOutcome::ZeroExtent);
}
self.recreate_swapchain(extent)?;
return Ok(VulkanSmokeFrameOutcome::Recreated);
}
let sync = &self.frame_sync[self.current_frame];
let image_available = sync.image_available;
let render_finished = sync.render_finished;
let in_flight_fence = sync.fence;
// SAFETY: The fence belongs to this live logical device and is waited from one thread.
unsafe {
self.device_ref()?
.device()
.wait_for_fences(&[in_flight_fence], true, 1_000_000_000)
}
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
context: "vkWaitForFences",
result: error,
})?;
// SAFETY: The swapchain, semaphore and fence inputs are live for the duration of the acquire call.
let acquire = unsafe {
self.swapchain_ref()?.loader().acquire_next_image(
self.swapchain_ref()?.swapchain(),
1_000_000_000,
image_available,
vk::Fence::null(),
)
};
let (image_index, acquire_suboptimal) = match acquire {
Ok(result) => result,
Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
self.recreate_swapchain(self.report.swapchain_extent)?;
return Ok(VulkanSmokeFrameOutcome::Recreated);
}
Err(error) => {
return Err(VulkanSmokeRendererError::VulkanOperation {
context: "vkAcquireNextImageKHR",
result: error,
});
}
};
let image_index_usize = usize::try_from(image_index).unwrap_or(0);
let image_fence = self.images_in_flight[image_index_usize];
if image_fence != vk::Fence::null() {
// SAFETY: The fence belongs to this renderer and can be waited independently.
unsafe {
self.device_ref()?
.device()
.wait_for_fences(&[image_fence], true, 1_000_000_000)
}
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
context: "vkWaitForFences(image)",
result: error,
})?;
}
self.images_in_flight[image_index_usize] = in_flight_fence;
// SAFETY: The fence belongs to this frame context and is not in use after the wait above.
unsafe { self.device_ref()?.device().reset_fences(&[in_flight_fence]) }.map_err(
|error| VulkanSmokeRendererError::VulkanOperation {
context: "vkResetFences",
result: error,
},
)?;
self.record_command_buffer(image_index_usize)?;
let wait_semaphores = [image_available];
let wait_stages = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT];
let command_buffers = [self.resources_ref()?.command_buffers[image_index_usize]];
let signal_semaphores = [render_finished];
let submit_info = [vk::SubmitInfo::default()
.wait_semaphores(&wait_semaphores)
.wait_dst_stage_mask(&wait_stages)
.command_buffers(&command_buffers)
.signal_semaphores(&signal_semaphores)];
// SAFETY: Submission references live queue, sync objects and recorded command buffer.
unsafe {
self.device_ref()?.device().queue_submit(
self.device_ref()?.graphics_queue(),
&submit_info,
in_flight_fence,
)
}
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
context: "vkQueueSubmit",
result: error,
})?;
let present_wait = [render_finished];
let swapchains = [self.swapchain_ref()?.swapchain()];
let image_indices = [image_index];
let present_info = vk::PresentInfoKHR::default()
.wait_semaphores(&present_wait)
.swapchains(&swapchains)
.image_indices(&image_indices);
// SAFETY: Presentation uses the rendered image index and a semaphore signaled by queue submission.
let present_suboptimal = match unsafe {
self.swapchain_ref()?
.loader()
.queue_present(self.device_ref()?.present_queue(), &present_info)
} {
Ok(suboptimal) => suboptimal,
Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
self.recreate_swapchain(self.report.swapchain_extent)?;
return Ok(VulkanSmokeFrameOutcome::Recreated);
}
Err(error) => {
return Err(VulkanSmokeRendererError::VulkanOperation {
context: "vkQueuePresentKHR",
result: error,
});
}
};
self.current_frame = (self.current_frame + 1) % self.frame_sync.len().max(1);
if acquire_suboptimal || present_suboptimal {
self.recreate_swapchain(self.report.swapchain_extent)?;
Ok(VulkanSmokeFrameOutcome::Recreated)
} else {
Ok(VulkanSmokeFrameOutcome::Presented)
}
}
fn recreate_swapchain(&mut self, extent: (u32, u32)) -> Result<(), VulkanSmokeRendererError> {
let device = self.device_ref()?;
// SAFETY: The logical device remains live and idling at swapchain recreation boundaries.
unsafe { device.device().device_wait_idle() }.map_err(|error| {
VulkanSmokeRendererError::VulkanOperation {
context: "vkDeviceWaitIdle",
result: error,
}
})?;
self.pending_extent = None;
self.rebuild_swapchain(extent)?;
self.swapchain_recreate_count = self.swapchain_recreate_count.saturating_add(1);
Ok(())
}
fn rebuild_swapchain(&mut self, extent: (u32, u32)) -> Result<(), VulkanSmokeRendererError> {
self.destroy_swapchain_resources();
let instance = self.instance_ref()?;
let surface = self.surface_ref()?;
let device = self.device_ref()?;
let old_swapchain = self
.swapchain
.as_ref()
.map_or(vk::SwapchainKHR::null(), VulkanSwapchainProbe::swapchain);
let new_swapchain = create_vulkan_swapchain_probe_for_extent(
instance,
surface,
device,
extent,
old_swapchain,
)
.map_err(VulkanSmokeRendererError::Swapchain)?;
self.swapchain = Some(new_swapchain);
self.rebuild_swapchain_resources(true)?;
Ok(())
}
fn rebuild_swapchain_resources(
&mut self,
reuse_command_pool: bool,
) -> Result<(), VulkanSmokeRendererError> {
let device = self.device_ref()?;
let swapchain = self.swapchain_ref()?;
let resources = RollbackOnDrop::new(
create_swapchain_resources(
device,
swapchain,
self.command_pool,
self.vertex_buffer_ref()?,
self.index_buffer_ref()?,
reuse_command_pool,
)?,
|resources| destroy_swapchain_resources(device, self.command_pool, resources),
);
let frame_sync = create_frame_sync(device)?;
let swapchain_extent = self.swapchain_ref()?.report.plan.extent;
let swapchain_image_count = self.swapchain_ref()?.report.image_count;
let resources = resources.commit();
self.images_in_flight = vec![vk::Fence::null(); resources.image_views.len()];
self.frame_sync = frame_sync;
self.report.swapchain_extent = swapchain_extent;
self.report.swapchain_image_count = swapchain_image_count;
self.swapchain_resources = Some(resources);
Ok(())
}
#[allow(clippy::too_many_lines)]
fn record_command_buffer(
&mut self,
image_index: usize,
) -> Result<(), VulkanSmokeRendererError> {
let device = self.device_ref()?;
let swapchain = self.swapchain_ref()?;
let resources = self.resources_ref()?;
let command_buffer = resources.command_buffers[image_index];
// SAFETY: The command buffer belongs to the resettable pool owned by this renderer.
unsafe {
device
.device()
.reset_command_buffer(command_buffer, vk::CommandBufferResetFlags::empty())
}
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
context: "vkResetCommandBuffer",
result: error,
})?;
let begin_info = vk::CommandBufferBeginInfo::default();
// SAFETY: The command buffer is in the initial state after reset and recorded on one thread.
unsafe {
device
.device()
.begin_command_buffer(command_buffer, &begin_info)
}
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
context: "vkBeginCommandBuffer",
result: error,
})?;
let clear_values = [vk::ClearValue {
color: vk::ClearColorValue {
float32: [0.05, 0.08, 0.11, 1.0],
},
}];
let render_area = vk::Rect2D {
offset: vk::Offset2D { x: 0, y: 0 },
extent: vk::Extent2D {
width: swapchain.report.plan.extent.0,
height: swapchain.report.plan.extent.1,
},
};
let render_pass_info = vk::RenderPassBeginInfo::default()
.render_pass(resources.render_pass)
.framebuffer(resources.framebuffers[image_index])
.render_area(render_area)
.clear_values(&clear_values);
// SAFETY: All commands target live frame resources owned by this renderer.
unsafe {
device.device().cmd_begin_render_pass(
command_buffer,
&render_pass_info,
vk::SubpassContents::INLINE,
);
device.device().cmd_bind_pipeline(
command_buffer,
vk::PipelineBindPoint::GRAPHICS,
resources.pipeline,
);
let vertex_buffers = [self.vertex_buffer_ref()?.buffer];
let offsets = [0_u64];
device
.device()
.cmd_bind_vertex_buffers(command_buffer, 0, &vertex_buffers, &offsets);
device.device().cmd_bind_index_buffer(
command_buffer,
self.index_buffer_ref()?.buffer,
0,
vk::IndexType::UINT16,
);
device
.device()
.cmd_draw_indexed(command_buffer, 3, 1, 0, 0, 0);
device.device().cmd_end_render_pass(command_buffer);
}
// SAFETY: The render pass owns the attachment layout transitions for this clear-and-present path.
unsafe { device.device().end_command_buffer(command_buffer) }.map_err(|error| {
VulkanSmokeRendererError::VulkanOperation {
context: "vkEndCommandBuffer",
result: error,
}
})?;
Ok(())
}
fn destroy_swapchain_resources(&mut self) {
let Some(device) = self.device.as_ref() else {
return;
};
for sync in self.frame_sync.drain(..) {
// SAFETY: These sync objects belong to this device and are destroyed once.
unsafe {
device
.device()
.destroy_semaphore(sync.image_available, None);
device
.device()
.destroy_semaphore(sync.render_finished, None);
device.device().destroy_fence(sync.fence, None);
}
}
if let Some(resources) = self.swapchain_resources.take() {
destroy_swapchain_resources(device, self.command_pool, resources);
}
self.images_in_flight.clear();
self.current_frame = 0;
}
fn teardown(&mut self) {
if let Some(device) = self.device.as_ref() {
// SAFETY: The logical device remains live until teardown finishes and idling prevents in-flight work from touching swapchain, buffers, sync objects or the command pool after destruction starts.
let _ = unsafe { device.device().device_wait_idle() };
}
self.destroy_swapchain_resources();
if let Some(device) = self.device.as_ref() {
if let Some(buffer) = self.index_buffer.take() {
// SAFETY: Buffer and memory belong to this device and are destroyed once after the device has been idled and frame work has been torn down.
unsafe {
device.device().destroy_buffer(buffer.buffer, None);
device.device().free_memory(buffer.memory, None);
}
}
if let Some(buffer) = self.vertex_buffer.take() {
// SAFETY: Buffer and memory belong to this device and are destroyed once after the device has been idled and frame work has been torn down.
unsafe {
device.device().destroy_buffer(buffer.buffer, None);
device.device().free_memory(buffer.memory, None);
}
}
// SAFETY: The command pool belongs to this device and is destroyed once after the device is idle and all command buffers allocated from it were freed above.
unsafe {
device
.device()
.destroy_command_pool(self.command_pool, None);
};
}
// Drop child Vulkan owners explicitly before their parents instead of relying on field order.
take_runtime_owners_in_dependency_order(
&mut self.instance,
&mut self.validation,
&mut self.surface,
&mut self.device,
&mut self.swapchain,
);
}
}
impl Drop for VulkanSmokeRenderer {
fn drop(&mut self) {
self.teardown();
}
}
#[cfg(test)]
mod tests {
use super::{take_runtime_owners_in_dependency_order, RollbackOnDrop};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum TeardownStep {
Instance,
Validation,
Surface,
Device,
Swapchain,
}
struct DropTracker {
step: TeardownStep,
log: Rc<RefCell<Vec<TeardownStep>>>,
}
impl Drop for DropTracker {
fn drop(&mut self) {
self.log.borrow_mut().push(self.step);
}
}
fn tracker(step: TeardownStep, log: &Rc<RefCell<Vec<TeardownStep>>>) -> DropTracker {
DropTracker {
step,
log: Rc::clone(log),
}
}
fn record_teardown_steps(present_steps: &[TeardownStep]) -> Vec<TeardownStep> {
let log = Rc::new(RefCell::new(Vec::new()));
let mut instance = present_steps
.contains(&TeardownStep::Instance)
.then(|| tracker(TeardownStep::Instance, &log));
let mut validation = present_steps
.contains(&TeardownStep::Validation)
.then(|| tracker(TeardownStep::Validation, &log));
let mut surface = present_steps
.contains(&TeardownStep::Surface)
.then(|| tracker(TeardownStep::Surface, &log));
let mut device = present_steps
.contains(&TeardownStep::Device)
.then(|| tracker(TeardownStep::Device, &log));
let mut swapchain = present_steps
.contains(&TeardownStep::Swapchain)
.then(|| tracker(TeardownStep::Swapchain, &log));
take_runtime_owners_in_dependency_order(
&mut instance,
&mut validation,
&mut surface,
&mut device,
&mut swapchain,
);
Rc::into_inner(log)
.expect("all drop trackers released")
.into_inner()
}
#[test]
fn runtime_owners_drop_in_explicit_dependency_order() {
assert_eq!(
record_teardown_steps(&[
TeardownStep::Instance,
TeardownStep::Validation,
TeardownStep::Surface,
TeardownStep::Device,
TeardownStep::Swapchain,
]),
vec![
TeardownStep::Swapchain,
TeardownStep::Device,
TeardownStep::Surface,
TeardownStep::Validation,
TeardownStep::Instance,
]
);
}
#[test]
fn runtime_owners_drop_remaining_children_after_partial_init_failures() {
let cases = [
(vec![TeardownStep::Instance], vec![TeardownStep::Instance]),
(
vec![TeardownStep::Instance, TeardownStep::Validation],
vec![TeardownStep::Validation, TeardownStep::Instance],
),
(
vec![
TeardownStep::Instance,
TeardownStep::Validation,
TeardownStep::Surface,
],
vec![
TeardownStep::Surface,
TeardownStep::Validation,
TeardownStep::Instance,
],
),
(
vec![
TeardownStep::Instance,
TeardownStep::Validation,
TeardownStep::Surface,
TeardownStep::Device,
],
vec![
TeardownStep::Device,
TeardownStep::Surface,
TeardownStep::Validation,
TeardownStep::Instance,
],
),
(
vec![
TeardownStep::Instance,
TeardownStep::Surface,
TeardownStep::Device,
TeardownStep::Swapchain,
],
vec![
TeardownStep::Swapchain,
TeardownStep::Device,
TeardownStep::Surface,
TeardownStep::Instance,
],
),
];
for (present_steps, expected) in cases {
assert_eq!(record_teardown_steps(&present_steps), expected);
}
}
#[test]
fn rollback_guard_runs_cleanup_when_later_step_fails() {
let log = Rc::new(RefCell::new(Vec::new()));
{
let _guard = RollbackOnDrop::new(tracker(TeardownStep::Swapchain, &log), |tracker| {
drop(tracker)
});
}
assert_eq!(log.borrow().as_slice(), &[TeardownStep::Swapchain]);
}
#[test]
fn rollback_guard_skips_cleanup_after_commit() {
let log = Rc::new(RefCell::new(Vec::new()));
let tracker = RollbackOnDrop::new(tracker(TeardownStep::Swapchain, &log), |tracker| {
drop(tracker)
})
.commit();
assert!(log.borrow().is_empty());
drop(tracker);
assert_eq!(log.borrow().as_slice(), &[TeardownStep::Swapchain]);
}
}