diff --git a/Cargo.toml b/Cargo.toml index 7c0b5b9a2..3c8dabb5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,5 +3,6 @@ members = [ "examples", "ash", "ash-window", + "ash-swapchain", "generator", ] diff --git a/ash-swapchain/Cargo.toml b/ash-swapchain/Cargo.toml new file mode 100644 index 000000000..4a37763ea --- /dev/null +++ b/ash-swapchain/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ash-swapchain" +version = "0.1.0" +edition = "2021" + +[dependencies] +ash = { path = "../ash", version = "0.34" } + +[dev-dependencies] +winit = "0.26" +ash-window = { path = "../ash-window", version = "0.8" } diff --git a/ash-swapchain/README.md b/ash-swapchain/README.md new file mode 100644 index 000000000..b009dc920 --- /dev/null +++ b/ash-swapchain/README.md @@ -0,0 +1,33 @@ +ash-swapchain += + +Synchronization for swapchain resize and presentation +- + +[![Crates.io Version](https://img.shields.io/crates/v/ash-window.svg)](https://crates.io/crates/ash-swapchain) +[![Documentation](https://docs.rs/ash-window/badge.svg)](https://docs.rs/ash-swapchain) +[![Build Status](https://github.com/MaikKlein/ash/workflows/CI/badge.svg)](https://github.com/MaikKlein/ash/actions?workflow=CI) +[![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE-MIT) +[![LICENSE](https://img.shields.io/badge/license-apache-blue.svg)](LICENSE-APACHE) + +## Usage + +A `Swapchain` manages (re)creating a `vk::SwapchainKHR` associated with a particular +`vk::SurfaceKHR`, and tracks the availability of resources associated with a particular *frame in +flight*. `Swapchain::acquire` yields an `AcquiredFrame` which specifies which swapchain image to +render to, which frame's resources are available for reuse, how to synchronize submitted work with +frame and swapchain image availability, and whether previously exposed swapchain images have been +invalidated. + +## License + +Licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +## Contribution + +Unless you explicitly state otherwise, any Contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/ash-swapchain/examples/demo.rs b/ash-swapchain/examples/demo.rs new file mode 100644 index 000000000..9f6c89b9b --- /dev/null +++ b/ash-swapchain/examples/demo.rs @@ -0,0 +1,343 @@ +use std::time::Instant; + +use ash::{extensions::khr, vk}; +use ash_swapchain::Swapchain; +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::{Window, WindowBuilder}, +}; + +fn main() { + let event_loop = EventLoop::new(); + let window = WindowBuilder::new().build(&event_loop).unwrap(); + + let mut app = App::new(&window); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Poll; + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + Event::WindowEvent { + event: WindowEvent::Resized(size), + .. + } => { + app.resize(vk::Extent2D { + width: size.width, + height: size.height, + }); + } + Event::MainEventsCleared => { + app.draw(); + } + _ => (), + } + }); +} + +pub struct App { + _entry: ash::Entry, + instance: ash::Instance, + surface: vk::SurfaceKHR, + epoch: Instant, + + functions: Functions, + swapchain: Swapchain, + queue: vk::Queue, + + command_pool: vk::CommandPool, + frames: Vec, +} + +impl App { + pub fn new(window: &Window) -> Self { + unsafe { + let entry = ash::Entry::new(); + let exts = ash_window::enumerate_required_extensions(&window).unwrap(); + let ext_ptrs = exts + .iter() + .map(|x| x.as_ptr() as *const _) + .collect::>(); + let instance = entry + .create_instance( + &vk::InstanceCreateInfo::builder() + .application_info(&vk::ApplicationInfo { + api_version: vk::make_api_version(0, 1, 0, 0), + ..Default::default() + }) + .enabled_extension_names(&ext_ptrs), + None, + ) + .unwrap(); + let surface_fn = khr::Surface::new(&entry, &instance); + let surface = ash_window::create_surface(&entry, &instance, &window, None).unwrap(); + + let (physical_device, queue_family_index) = instance + .enumerate_physical_devices() + .unwrap() + .into_iter() + .find_map(|dev| { + let (family, _) = instance + .get_physical_device_queue_family_properties(dev) + .into_iter() + .enumerate() + .find(|(_index, info)| { + info.queue_flags.contains(vk::QueueFlags::GRAPHICS) + })?; + let family = family as u32; + let supported = surface_fn + .get_physical_device_surface_support(dev, family, surface) + .unwrap(); + if !supported { + return None; + } + Some((dev, family)) + }) + .unwrap(); + + let device = instance + .create_device( + physical_device, + &vk::DeviceCreateInfo::builder() + .enabled_extension_names(&[khr::Swapchain::name().as_ptr() as _]) + .queue_create_infos(&[vk::DeviceQueueCreateInfo::builder() + .queue_family_index(queue_family_index) + .queue_priorities(&[1.0]) + .build()]), + None, + ) + .unwrap(); + let swapchain_fn = khr::Swapchain::new(&instance, &device); + let queue = device.get_device_queue(queue_family_index, queue_family_index); + + let size = window.inner_size(); + let mut options = ash_swapchain::Options::default(); + options.usage(vk::ImageUsageFlags::TRANSFER_DST); // Typically this would be left as the default, COLOR_ATTACHMENT + let swapchain = Swapchain::new( + &ash_swapchain::Functions { + device: &device, + swapchain: &swapchain_fn, + surface: &surface_fn, + }, + options, + surface, + physical_device, + vk::Extent2D { + width: size.width, + height: size.height, + }, + ); + + let command_pool = device + .create_command_pool( + &vk::CommandPoolCreateInfo::builder() + .flags( + vk::CommandPoolCreateFlags::TRANSIENT + | vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER, + ) + .queue_family_index(queue_family_index), + None, + ) + .unwrap(); + let cmds = device + .allocate_command_buffers( + &vk::CommandBufferAllocateInfo::builder() + .command_pool(command_pool) + .level(vk::CommandBufferLevel::PRIMARY) + .command_buffer_count(swapchain.frames_in_flight() as u32), + ) + .unwrap(); + let frames = cmds + .into_iter() + .map(|cmd| Frame { + cmd, + complete: device + .create_semaphore(&vk::SemaphoreCreateInfo::default(), None) + .unwrap(), + }) + .collect(); + + Self { + _entry: entry, + instance, + surface, + epoch: Instant::now(), + + functions: Functions { + device, + swapchain: swapchain_fn, + surface: surface_fn, + }, + swapchain, + queue, + + command_pool, + frames, + } + } + } + + fn resize(&mut self, size: vk::Extent2D) { + self.swapchain.update(size); + } + + fn draw(&mut self) { + let device = &self.functions.device; + unsafe { + let acq = self + .swapchain + .acquire(&self.functions.ash_swapchain(), !0) + .unwrap(); + let cmd = self.frames[acq.frame_index].cmd; + let swapchain_image = self.swapchain.images()[acq.image_index]; + device + .begin_command_buffer( + cmd, + &vk::CommandBufferBeginInfo::builder() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), + ) + .unwrap(); + + // + // Record commands to render to swapchain_image + // + + // Typically this barrier would be implemented with a subpass dependency from EXTERNAL, + // with both pipeline stages set to COLOR_ATTACHMENT_OUTPUT so that it doesn't block + // work that doesn't write to the swapchain image. The source stage must overlap with + // the wait_dst_stage_mask passed to `queue_submit` below to ensure that the image + // transition doesn't happen until after the acquire semaphore is signaled. + device.cmd_pipeline_barrier( + cmd, + vk::PipelineStageFlags::TRANSFER, + vk::PipelineStageFlags::TRANSFER, + vk::DependencyFlags::default(), + &[], + &[], + &[vk::ImageMemoryBarrier::builder() + .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .old_layout(vk::ImageLayout::UNDEFINED) + .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) + .image(swapchain_image) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }) + .build()], + ); + let t = (self.epoch.elapsed().as_secs_f32().sin() + 1.0) * 0.5; + device.cmd_clear_color_image( + cmd, + swapchain_image, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + &vk::ClearColorValue { + float32: [0.0, t, 0.0, 1.0], + }, + &[vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }], + ); + // Typically this barrier would be implemented with the implicit subpass dependency to + // EXTERNAL + device.cmd_pipeline_barrier( + cmd, + vk::PipelineStageFlags::TRANSFER, + vk::PipelineStageFlags::BOTTOM_OF_PIPE, + vk::DependencyFlags::default(), + &[], + &[], + &[vk::ImageMemoryBarrier::builder() + .src_access_mask(vk::AccessFlags::TRANSFER_WRITE) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) + .new_layout(vk::ImageLayout::PRESENT_SRC_KHR) + .image(swapchain_image) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }) + .build()], + ); + + // + // Submit commands and queue present + // + + device.end_command_buffer(cmd).unwrap(); + device + .queue_submit( + self.queue, + &[vk::SubmitInfo::builder() + .wait_semaphores(&[acq.ready]) + .wait_dst_stage_mask(&[vk::PipelineStageFlags::TRANSFER]) + .signal_semaphores(&[self.frames[acq.frame_index].complete]) + .command_buffers(&[cmd]) + .build()], + acq.complete, + ) + .unwrap(); + self.swapchain + .queue_present( + &self.functions.ash_swapchain(), + self.queue, + self.frames[acq.frame_index].complete, + acq.image_index, + ) + .unwrap(); + } + } +} + +impl Drop for App { + fn drop(&mut self) { + unsafe { + let device = &self.functions.device; + let _ = device.device_wait_idle(); + for frame in &self.frames { + device.destroy_semaphore(frame.complete, None); + } + device.destroy_command_pool(self.command_pool, None); + self.swapchain.destroy(&self.functions.ash_swapchain()); + self.functions.surface.destroy_surface(self.surface, None); + device.destroy_device(None); + self.instance.destroy_instance(None); + } + } +} + +struct Functions { + device: ash::Device, + surface: khr::Surface, + swapchain: khr::Swapchain, +} + +impl Functions { + fn ash_swapchain(&self) -> ash_swapchain::Functions<'_> { + ash_swapchain::Functions { + device: &self.device, + swapchain: &self.swapchain, + surface: &self.surface, + } + } +} + +struct Frame { + cmd: vk::CommandBuffer, + complete: vk::Semaphore, +} diff --git a/ash-swapchain/src/lib.rs b/ash-swapchain/src/lib.rs new file mode 100644 index 000000000..b0c8f0be0 --- /dev/null +++ b/ash-swapchain/src/lib.rs @@ -0,0 +1,415 @@ +use std::{collections::VecDeque, mem}; + +use ash::{extensions::khr, prelude::VkResult, vk, Device}; + +/// Manages synchronizing and rebuilding a Vulkan swapchain +pub struct Swapchain { + options: Options, + + frames: Vec, + frame_index: usize, + + surface: vk::SurfaceKHR, + physical_device: vk::PhysicalDevice, + handle: vk::SwapchainKHR, + generation: u64, + images: Vec, + extent: vk::Extent2D, + format: vk::SurfaceFormatKHR, + needs_rebuild: bool, + + old_swapchains: VecDeque<(vk::SwapchainKHR, u64)>, +} + +impl Swapchain { + /// Construct a new [`Swapchain`] for rendering at most `frames_in_flight` frames + /// concurrently. `extent` should be the current dimensions of `surface`. + pub fn new( + fp: &Functions<'_>, + options: Options, + surface: vk::SurfaceKHR, + physical_device: vk::PhysicalDevice, + extent: vk::Extent2D, + ) -> Self { + Self { + frames: (0..options.frames_in_flight) + .map(|_| unsafe { + Frame { + complete: fp + .device + .create_fence( + &vk::FenceCreateInfo::builder() + .flags(vk::FenceCreateFlags::SIGNALED), + None, + ) + .unwrap(), + acquire: fp + .device + .create_semaphore(&vk::SemaphoreCreateInfo::default(), None) + .unwrap(), + generation: 0, + } + }) + .collect(), + frame_index: 0, + + surface, + physical_device, + handle: vk::SwapchainKHR::null(), + generation: 0, + images: Vec::new(), + extent, + format: vk::SurfaceFormatKHR::default(), + needs_rebuild: true, + + old_swapchains: VecDeque::new(), + + options, + } + } + + /// Destroy all swapchain resources. Must not be called while any frames are still in flight on + /// the GPU. + /// + /// # Safety + /// + /// Access to images obtained from [`images`](Self::images) must be externally synchronized, and + /// the contents of `fp` must be associated with the same `vk::Device` as that passed to + /// [`new`](Self::new). + pub unsafe fn destroy(&mut self, fp: &Functions<'_>) { + for frame in &self.frames { + fp.device.destroy_fence(frame.complete, None); + fp.device.destroy_semaphore(frame.acquire, None); + } + if self.handle != vk::SwapchainKHR::null() { + fp.swapchain.destroy_swapchain(self.handle, None); + } + for &(swapchain, _) in &self.old_swapchains { + fp.swapchain.destroy_swapchain(swapchain, None); + } + } + + /// Force the swapchain to be rebuilt on the next [`acquire`](Self::acquire) call, passing in + /// the surface's current size. + pub fn update(&mut self, extent: vk::Extent2D) { + self.extent = extent; + self.needs_rebuild = true; + } + + /// Maximum number of frames that may be concurrently rendered + pub fn frames_in_flight(&self) -> usize { + self.frames.len() + } + + /// Latest set of swapchain images, keyed by [`AcquiredFrame::image_index`] + pub fn images(&self) -> &[vk::Image] { + &self.images + } + + /// Format of images in [`images`](Self::images), and the color space that will be used to + /// present them + pub fn format(&self) -> vk::SurfaceFormatKHR { + self.format + } + + /// Dimensions of images in [`images`](Self::images) + pub fn extent(&self) -> vk::Extent2D { + self.extent + } + + /// Acquire resources to render a frame + /// + /// Returns [`vk::Result::ERROR_OUT_OF_DATE_KHR`] if and only if the configured format or + /// present modes could not be satisfied. + /// + /// # Safety + /// + /// The contents of `fp` must be associated with the same `vk::Device` as that passed to + /// [`new`](Self::new). + pub unsafe fn acquire( + &mut self, + fp: &Functions<'_>, + timeout_ns: u64, + ) -> VkResult { + let frame_index = self.frame_index; + let next_frame_index = (self.frame_index + 1) % self.frames.len(); + let frame = &self.frames[frame_index]; + let acquire = frame.acquire; + fp.device + .wait_for_fences(&[frame.complete], true, timeout_ns)?; + + // Destroy swapchains that are guaranteed not to be in use now that this frame has finished + while let Some(&(swapchain, generation)) = self.old_swapchains.front() { + if self.frames[next_frame_index].generation == generation { + break; + } + fp.swapchain.destroy_swapchain(swapchain, None); + self.old_swapchains.pop_front(); + } + + loop { + if !self.needs_rebuild { + match fp + .swapchain + .acquire_next_image(self.handle, !0, acquire, vk::Fence::null()) + { + Ok((index, suboptimal)) => { + self.needs_rebuild = suboptimal; + let invalidate_images = + self.frames[frame_index].generation != self.generation; + self.frames[frame_index].generation = self.generation; + self.frame_index = next_frame_index; + fp.device + .reset_fences(&[self.frames[frame_index].complete]) + .unwrap(); + return Ok(AcquiredFrame { + image_index: index as usize, + frame_index, + ready: acquire, + complete: self.frames[frame_index].complete, + invalidate_images, + }); + } + Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {} + Err(e) => return Err(e), + } + }; + self.needs_rebuild = true; + + // Rebuild swapchain + let surface_capabilities = fp + .surface + .get_physical_device_surface_capabilities(self.physical_device, self.surface)?; + self.extent = match surface_capabilities.current_extent.width { + // If Vulkan doesn't know, the windowing system probably does. Known to apply at + // least to Wayland. + std::u32::MAX => vk::Extent2D { + width: self.extent.width, + height: self.extent.height, + }, + _ => surface_capabilities.current_extent, + }; + let pre_transform = if surface_capabilities + .supported_transforms + .contains(vk::SurfaceTransformFlagsKHR::IDENTITY) + { + vk::SurfaceTransformFlagsKHR::IDENTITY + } else { + surface_capabilities.current_transform + }; + let present_mode = fp + .surface + .get_physical_device_surface_present_modes(self.physical_device, self.surface)? + .iter() + .filter_map(|&mode| { + Some(( + mode, + self.options + .present_mode_preference + .iter() + .position(|&pref| pref == mode)?, + )) + }) + .min_by_key(|&(_, priority)| priority) + .map(|(mode, _)| mode) + .ok_or(vk::Result::ERROR_OUT_OF_DATE_KHR)?; + + let desired_image_count = + (surface_capabilities.min_image_count + 1).max(self.frames.len() as u32); + let image_count = if surface_capabilities.max_image_count > 0 { + surface_capabilities + .max_image_count + .min(desired_image_count) + } else { + desired_image_count + }; + + self.format = fp + .surface + .get_physical_device_surface_formats(self.physical_device, self.surface)? + .iter() + .filter_map(|&format| { + Some(( + format, + self.options + .format_preference + .iter() + .position(|&pref| pref == format)?, + )) + }) + .min_by_key(|&(_, priority)| priority) + .map(|(mode, _)| mode) + .ok_or(vk::Result::ERROR_OUT_OF_DATE_KHR)?; + + if self.handle != vk::SwapchainKHR::null() { + self.old_swapchains + .push_back((self.handle, self.generation)); + } + let handle = fp.swapchain.create_swapchain( + &vk::SwapchainCreateInfoKHR::builder() + .surface(self.surface) + .min_image_count(image_count) + .image_color_space(self.format.color_space) + .image_format(self.format.format) + .image_extent(self.extent) + .image_usage(self.options.usage) + .image_sharing_mode(self.options.sharing_mode) + .pre_transform(pre_transform) + .composite_alpha(self.options.composite_alpha) + .present_mode(present_mode) + .clipped(true) + .image_array_layers(1) + .old_swapchain(mem::replace(&mut self.handle, vk::SwapchainKHR::null())), + None, + )?; + self.generation = self.generation.wrapping_add(1); + self.handle = handle; + self.images = fp.swapchain.get_swapchain_images(handle)?; + self.needs_rebuild = false; + } + } + + /// Queue presentation of a previously acquired image + /// + /// # Safety + /// + /// In addition to the usual requirements of [`khr::Swapchain::queue_present`]: + /// + /// - The contents of `fp` must be associated with the same `vk::Device` as that passed to + /// [`new`](Self::new). + /// - `image_index` must have been obtained from an [`AcquiredFrame::image_index`] from a + /// previous [`acquire`](Self::acquire) call which has not yet been passed to queue_present + /// - A command buffer that will signal `render_complete` after finishing access to the + /// `image_index` element of [`images`](Self::images) must have been submitted + pub unsafe fn queue_present( + &mut self, + fp: &Functions<'_>, + queue: vk::Queue, + render_complete: vk::Semaphore, + image_index: usize, + ) -> VkResult<()> { + match fp.swapchain.queue_present( + queue, + &vk::PresentInfoKHR::builder() + .wait_semaphores(&[render_complete]) + .swapchains(&[self.handle]) + .image_indices(&[image_index as u32]), + ) { + Ok(false) => Ok(()), + Ok(true) | Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => { + self.needs_rebuild = true; + Ok(()) + } + Err(e) => Err(e), + } + } +} + +/// Functions required by [`Swapchain`] methods +pub struct Functions<'a> { + pub device: &'a Device, + pub swapchain: &'a khr::Swapchain, + pub surface: &'a khr::Surface, +} + +/// [`Swapchain`] configuration +#[derive(Debug, Clone)] +pub struct Options { + frames_in_flight: usize, + format_preference: Vec, + present_mode_preference: Vec, + usage: vk::ImageUsageFlags, + sharing_mode: vk::SharingMode, + composite_alpha: vk::CompositeAlphaFlagsKHR, +} + +impl Options { + pub fn new() -> Self { + Self::default() + } + + /// Number of frames that may be concurrently worked on, including recording on the CPU. Defaults to 2. + pub fn frames_in_flight(&mut self, frames: usize) -> &mut Self { + self.frames_in_flight = frames; + self + } + + /// Preference-ordered list of image formats and color spaces. Defaults to 8-bit sRGB. + pub fn format_preference(&mut self, formats: &[vk::SurfaceFormatKHR]) -> &mut Self { + self.format_preference = formats.into(); + self + } + + /// Preference-ordered list of presentation modes. Defaults to [`vk::PresentModeKHR::FIFO`]. + pub fn present_mode_preference(&mut self, modes: &[vk::PresentModeKHR]) -> &mut Self { + self.present_mode_preference = modes.into(); + self + } + + /// Required swapchain image usage flags. Defaults to [`vk::ImageUsageFlags::COLOR_ATTACHMENT`]. + pub fn usage(&mut self, usage: vk::ImageUsageFlags) -> &mut Self { + self.usage = usage; + self + } + + /// Requires swapchain image sharing mode. Defaults to [`vk::SharingMode::EXCLUSIVE`]. + pub fn sharing_mode(&mut self, mode: vk::SharingMode) -> &mut Self { + self.sharing_mode = mode; + self + } + + /// Requires swapchain image composite alpha. Defaults to [`vk::CompositeAlphaFlagsKHR::OPAQUE`]. + pub fn composite_alpha(&mut self, value: vk::CompositeAlphaFlagsKHR) -> &mut Self { + self.composite_alpha = value; + self + } +} + +impl Default for Options { + fn default() -> Self { + Self { + frames_in_flight: 2, + format_preference: vec![ + vk::SurfaceFormatKHR { + format: vk::Format::B8G8R8A8_SRGB, + color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR, + }, + vk::SurfaceFormatKHR { + format: vk::Format::R8G8B8A8_SRGB, + color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR, + }, + ], + present_mode_preference: vec![vk::PresentModeKHR::FIFO], + usage: vk::ImageUsageFlags::COLOR_ATTACHMENT, + sharing_mode: vk::SharingMode::EXCLUSIVE, + composite_alpha: vk::CompositeAlphaFlagsKHR::OPAQUE, + } + } +} + +struct Frame { + complete: vk::Fence, + acquire: vk::Semaphore, + generation: u64, +} + +/// Information necessary to render a frame, from [`Swapchain::acquire`] +#[derive(Debug, Copy, Clone)] +pub struct AcquiredFrame { + /// Index of the image to write to in [`Swapchain::images`] + pub image_index: usize, + /// Index of the frame in flight, for use tracking your own per-frame resources, which may be + /// accessed immediately after [`Swapchain::acquire`] returns + pub frame_index: usize, + /// Must be waited on before accessing the image associated with `image_index` + pub ready: vk::Semaphore, + /// Must be signaled when access to the image associated with `image_index` and any per-frame + /// resources associated with `frame_index` is complete + pub complete: vk::Fence, + /// Set whenever [`Swapchain::images`] has, and [`Swapchain::extent`] and [`Swapchain::format`] + /// may have, changed since the last [`Swapchain::acquire`] call. Use this to invalidate derived + /// resources like [`vk::ImageView`]s and [`vk::Framebuffer`]s, taking care not to destroy them + /// until at least [`Options::frames_in_flight`] new frames have been acquired, including this + /// one. + pub invalidate_images: bool, +}