diff --git a/crates/renderer/src/lib.rs b/crates/renderer/src/lib.rs
index d10bc45..215ce52 100644
--- a/crates/renderer/src/lib.rs
+++ b/crates/renderer/src/lib.rs
@@ -13,16 +13,10 @@ use std::{
     collections::{BTreeMap, BTreeSet, HashMap},
     ffi::{CStr, CString},
     fmt::Debug,
-    hint::black_box,
     marker::PhantomData,
-    sync::{
-        atomic::{AtomicU32, AtomicU64},
-        Arc,
-    },
+    sync::Arc,
 };
 
-use egui::Color32;
-use indexmap::IndexMap;
 use parking_lot::{Mutex, MutexGuard, RwLock};
 
 use ash::{
@@ -35,16 +29,19 @@ use dyn_clone::DynClone;
 use rand::{Rng, SeedableRng};
 use raw_window_handle::RawDisplayHandle;
 
+pub use ash;
+
 mod buffers;
-mod commands;
-mod device;
+pub mod commands;
+pub mod device;
 #[path = "egui.rs"]
 mod egui_pass;
 mod images;
+mod memory;
 mod pipeline;
-mod render_graph;
+pub mod render_graph;
 pub mod rendering;
-mod sync;
+pub mod sync;
 pub mod util;
 
 use device::{Device, DeviceOwned, DeviceQueueFamilies};
@@ -476,603 +473,7 @@ impl AsRef<ash::khr::surface::Instance> for Instance {
     }
 }
 
-#[derive(Debug)]
-struct SwapchainHandle(Mutex<vk::SwapchainKHR>);
-
-impl SwapchainHandle {
-    unsafe fn from_handle(swapchain: vk::SwapchainKHR) -> SwapchainHandle {
-        Self(Mutex::new(swapchain))
-    }
-
-    fn lock(&self) -> MutexGuard<'_, vk::SwapchainKHR> {
-        self.0.lock()
-    }
-
-    fn with_locked<T, F: FnOnce(vk::SwapchainKHR) -> T>(&self, f: F) -> T {
-        let lock = self.0.lock();
-        f(*lock)
-    }
-}
-
-#[derive(Debug)]
-pub struct Swapchain {
-    device: Device,
-    // has a strong ref to the surface because the surface may not outlive the swapchain
-    surface: Arc<Surface>,
-    swapchain: SwapchainHandle,
-    #[allow(unused)]
-    present_mode: vk::PresentModeKHR,
-    #[allow(unused)]
-    color_space: vk::ColorSpaceKHR,
-    format: vk::Format,
-    images: Vec<Arc<images::Image>>,
-    image_views: Vec<vk::ImageView>,
-    extent: vk::Extent2D,
-    min_image_count: u32,
-
-    // sync objects:
-    // we need two semaphores per each image, one acquire-semaphore and one release-semaphore.
-    // semaphores must be unique to each frame and cannot be reused per swapchain.
-    acquire_semaphores: Vec<vk::Semaphore>,
-    release_semaphores: Vec<vk::Semaphore>,
-
-    // one fence per in-flight frame, to synchronize image acquisition
-    fences: Vec<Arc<sync::Fence>>,
-
-    current_frame: AtomicU32,
-
-    // for khr_present_id/khr_present_wait
-    present_id: AtomicU64,
-}
-
-impl Drop for Swapchain {
-    fn drop(&mut self) {
-        unsafe {
-            _ = self.device.wait_queue_idle(self.device.present_queue());
-            tracing::debug!("dropping swapchain {:?}", self.swapchain);
-            for view in &self.image_views {
-                self.device.dev().destroy_image_view(*view, None);
-            }
-
-            self.swapchain.with_locked(|swapchain| {
-                self.device.swapchain().destroy_swapchain(swapchain, None)
-            });
-
-            for &semaphore in self
-                .acquire_semaphores
-                .iter()
-                .chain(&self.release_semaphores)
-            {
-                self.device.dev().destroy_semaphore(semaphore, None);
-            }
-        }
-    }
-}
-
-fn current_extent_or_clamped(
-    caps: &vk::SurfaceCapabilitiesKHR,
-    fallback: vk::Extent2D,
-) -> vk::Extent2D {
-    if caps.current_extent.width == u32::MAX {
-        vk::Extent2D {
-            width: fallback
-                .width
-                .clamp(caps.min_image_extent.width, caps.max_image_extent.width),
-            height: fallback
-                .height
-                .clamp(caps.min_image_extent.height, caps.max_image_extent.height),
-        }
-    } else {
-        caps.current_extent
-    }
-}
-
-struct SwapchainParams {
-    present_mode: vk::PresentModeKHR,
-    format: vk::Format,
-    color_space: vk::ColorSpaceKHR,
-    /// the number of images to request from the device
-    image_count: u32,
-    /// the minimum number of images the surface permits
-    min_image_count: u32,
-    extent: vk::Extent2D,
-}
-
-#[derive(Debug)]
-#[must_use = "This struct represents an acquired image from the swapchain and
- must be presented in order to free resources on the device."]
-pub struct SwapchainFrame {
-    pub swapchain: Arc<Swapchain>,
-    pub index: u32,
-    pub image: Arc<images::Image>,
-    pub format: vk::Format,
-    pub view: vk::ImageView,
-    pub acquire: vk::Semaphore,
-    pub release: vk::Semaphore,
-}
-
-impl Eq for SwapchainFrame {}
-impl PartialEq for SwapchainFrame {
-    fn eq(&self, other: &Self) -> bool {
-        self.index == other.index && self.image == other.image
-    }
-}
-
-impl SwapchainFrame {
-    fn present(self, wait: Option<vk::Semaphore>) -> Result<()> {
-        self.swapchain.clone().present(self, wait)
-    }
-}
-
-impl Swapchain {
-    const PREFERRED_IMAGES_IN_FLIGHT: u32 = 3;
-
-    fn get_swapchain_params_from_surface(
-        instance: &Arc<Instance>,
-        surface: vk::SurfaceKHR,
-        pdev: vk::PhysicalDevice,
-        requested_extent: Option<vk::Extent2D>,
-    ) -> Result<SwapchainParams> {
-        let caps = unsafe {
-            instance
-                .surface
-                .get_physical_device_surface_capabilities(pdev, surface)?
-        };
-        let formats = unsafe {
-            instance
-                .surface
-                .get_physical_device_surface_formats(pdev, surface)?
-        };
-        let present_modes = unsafe {
-            instance
-                .surface
-                .get_physical_device_surface_present_modes(pdev, surface)?
-        };
-
-        let present_mode = present_modes
-            .iter()
-            .find(|&mode| mode == &vk::PresentModeKHR::MAILBOX)
-            .cloned()
-            .unwrap_or(vk::PresentModeKHR::FIFO);
-
-        let format = formats
-            .iter()
-            .max_by_key(|&&format| {
-                let is_rgba_unorm = format.format == vk::Format::R8G8B8A8_UNORM
-                    || format.format == vk::Format::B8G8R8A8_UNORM;
-                let is_srgb = format.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR;
-                is_rgba_unorm as u8 * 10 + is_srgb as u8
-            })
-            .or(formats.first())
-            .cloned()
-            .expect("no surface format available!");
-
-        // 0 here means no limit
-        let max_image_count = core::num::NonZero::new(caps.max_image_count)
-            .map(|n| n.get())
-            .unwrap_or(u32::MAX);
-
-        // we want PREFERRED_IMAGES_IN_FLIGHT images acquired at the same time,
-        let image_count =
-            (caps.min_image_count + Self::PREFERRED_IMAGES_IN_FLIGHT).min(max_image_count);
-
-        let extent = current_extent_or_clamped(
-            &caps,
-            requested_extent.unwrap_or(vk::Extent2D::default().width(1).height(1)),
-        );
-
-        Ok(SwapchainParams {
-            present_mode,
-            format: format.format,
-            color_space: format.color_space,
-            image_count,
-            extent,
-            min_image_count: caps.min_image_count,
-        })
-    }
-
-    pub fn new(
-        device: Device,
-        surface: Arc<Surface>,
-        pdev: vk::PhysicalDevice,
-        extent: vk::Extent2D,
-    ) -> Result<Self> {
-        Self::create(device, surface, pdev, Some(extent), None)
-    }
-
-    fn create(
-        device: Device,
-        surface: Arc<Surface>,
-        pdev: vk::PhysicalDevice,
-        extent: Option<vk::Extent2D>,
-        old_swapchain: Option<&SwapchainHandle>,
-    ) -> Result<Self> {
-        let SwapchainParams {
-            present_mode,
-            format,
-            color_space,
-            image_count,
-            min_image_count,
-            extent,
-        } = Self::get_swapchain_params_from_surface(
-            device.instance(),
-            surface.surface,
-            pdev,
-            extent,
-        )?;
-
-        let (swapchain, images) = {
-            let lock = old_swapchain.as_ref().map(|handle| handle.lock());
-
-            Self::create_vkswapchainkhr(
-                &device,
-                surface.surface,
-                &device.queue_families().swapchain_family_indices(),
-                extent,
-                lock.as_ref().map(|lock| **lock),
-                present_mode,
-                format,
-                color_space,
-                image_count,
-            )
-        }?;
-
-        let images = images
-            .iter()
-            .enumerate()
-            .map(|(i, image)| unsafe {
-                images::Image::from_swapchain_image(
-                    device.clone(),
-                    *image,
-                    Some(format!("swapchain-{swapchain:?}-image-{i}").into()),
-                    vk::Extent3D {
-                        width: extent.width,
-                        height: extent.height,
-                        depth: 1,
-                    },
-                    format,
-                )
-                .inspect(|img| {
-                    _ = img.get_view(images::ImageViewDesc {
-                        name: Some(format!("swapchain-{swapchain:?}-image-{i}-view").into()),
-                        kind: vk::ImageViewType::TYPE_2D,
-                        format,
-                        aspect: vk::ImageAspectFlags::COLOR,
-                        ..Default::default()
-                    });
-                })
-                .map(|img| Arc::new(img))
-            })
-            .collect::<VkResult<Vec<_>>>()?;
-
-        let image_views = images
-            .iter()
-            .enumerate()
-            .map(|(i, image)| {
-                image.get_view(images::ImageViewDesc {
-                    name: Some(format!("swapchain-{swapchain:?}-image-{i}-view").into()),
-                    kind: vk::ImageViewType::TYPE_2D,
-                    format,
-                    aspect: vk::ImageAspectFlags::COLOR,
-                    ..Default::default()
-                })
-            })
-            .collect::<VkResult<Vec<_>>>()?;
-
-        let num_images = images.len() as u32;
-        let inflight_frames = num_images - min_image_count;
-
-        let acquire_semaphores = {
-            (0..inflight_frames)
-                .map(|i| unsafe {
-                    device
-                        .dev()
-                        .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)
-                        .inspect(|r| {
-                            #[cfg(debug_assertions)]
-                            {
-                                device
-                                    .debug_name_object(
-                                        *r,
-                                        &format!(
-                                            "semaphore-{:x}_{i}-acquire",
-                                            swapchain.0.lock().as_raw()
-                                        ),
-                                    )
-                                    .unwrap();
-                            }
-                        })
-                })
-                .collect::<VkResult<Vec<_>>>()?
-        };
-
-        let release_semaphores = {
-            (0..inflight_frames)
-                .map(|i| unsafe {
-                    device
-                        .dev()
-                        .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)
-                        .inspect(|r| {
-                            #[cfg(debug_assertions)]
-                            {
-                                device
-                                    .debug_name_object(
-                                        *r,
-                                        &format!(
-                                            "semaphore-{:x}_{i}-release",
-                                            swapchain.0.lock().as_raw()
-                                        ),
-                                    )
-                                    .unwrap();
-                            }
-                        })
-                })
-                .collect::<VkResult<Vec<_>>>()?
-        };
-
-        let fences = {
-            (0..inflight_frames)
-                .map(|i| {
-                    Ok(Arc::new(sync::Fence::create(device.clone()).inspect(
-                        |r| {
-                            #[cfg(debug_assertions)]
-                            {
-                                device
-                                    .debug_name_object(
-                                        r.fence(),
-                                        &format!("fence-{:x}_{i}", swapchain.0.lock().as_raw()),
-                                    )
-                                    .unwrap();
-                            }
-                        },
-                    )?))
-                })
-                .collect::<VkResult<Vec<_>>>()?
-        };
-
-        tracing::trace!("fences: {fences:?}");
-
-        Ok(Self {
-            device,
-            surface,
-            swapchain,
-            present_mode,
-            color_space,
-            format,
-            images,
-            image_views,
-            min_image_count,
-            extent,
-            acquire_semaphores,
-            release_semaphores,
-            fences,
-            current_frame: AtomicU32::new(0),
-            present_id: AtomicU64::new(1),
-        })
-    }
-
-    pub fn max_in_flight_images(&self) -> u32 {
-        self.num_images() - self.min_image_count
-    }
-
-    pub fn num_images(&self) -> u32 {
-        self.images.len() as u32
-    }
-
-    fn recreate(&self, extent: Option<vk::Extent2D>) -> Result<Self> {
-        Self::create(
-            self.device.clone(),
-            self.surface.clone(),
-            self.device.phy(),
-            extent,
-            Some(&self.swapchain),
-        )
-    }
-
-    /// returns a future yielding the frame, and true if the swapchain is
-    /// suboptimal and should be recreated.
-    fn acquire_image(
-        self: Arc<Self>,
-    ) -> impl std::future::Future<Output = VkResult<(SwapchainFrame, bool)>> {
-        let frame = self
-            .current_frame
-            .fetch_update(
-                std::sync::atomic::Ordering::Release,
-                std::sync::atomic::Ordering::Relaxed,
-                |i| Some((i + 1) % self.max_in_flight_images()),
-            )
-            .unwrap() as usize;
-
-        tracing::trace!(frame, "acquiring image for frame {frame}");
-
-        async move {
-            let fence = self.fences[frame].clone();
-            let acquire = self.acquire_semaphores[frame];
-            let release = self.release_semaphores[frame];
-
-            // spawn on threadpool because it might block.
-            let (idx, suboptimal) = smol::unblock({
-                let this = self.clone();
-                let fence = fence.clone();
-                move || unsafe {
-                    this.swapchain.with_locked(|swapchain| {
-                        this.device.swapchain().acquire_next_image(
-                            swapchain,
-                            u64::MAX,
-                            acquire,
-                            fence.fence(),
-                        )
-                    })
-                }
-            })
-            .await?;
-
-            // wait for image to become available.
-            sync::FenceFuture::new(fence.clone()).await;
-
-            let idx = idx as usize;
-            let image = self.images[idx].clone();
-            let view = self.image_views[idx];
-
-            Ok((
-                SwapchainFrame {
-                    index: idx as u32,
-                    swapchain: self.clone(),
-                    format: self.format,
-                    image,
-                    view,
-                    acquire,
-                    release,
-                },
-                suboptimal,
-            ))
-        }
-    }
-
-    fn present(&self, frame: SwapchainFrame, wait: Option<vk::Semaphore>) -> Result<()> {
-        let swpchain = self.swapchain.lock();
-        let queue = self.device.present_queue().lock();
-
-        let wait_semaphores = wait
-            .as_ref()
-            .map(|sema| core::slice::from_ref(sema))
-            .unwrap_or_default();
-
-        // TODO: make this optional for devices with no support for present_wait/present_id
-        let present_id = self
-            .present_id
-            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
-        let mut present_id =
-            vk::PresentIdKHR::default().present_ids(core::slice::from_ref(&present_id));
-
-        let present_info = vk::PresentInfoKHR::default()
-            .image_indices(core::slice::from_ref(&frame.index))
-            .swapchains(core::slice::from_ref(&swpchain))
-            .wait_semaphores(wait_semaphores)
-            .push_next(&mut present_id);
-
-        // call winits pre_present_notify here
-
-        unsafe {
-            self.device
-                .swapchain()
-                .queue_present(*queue, &present_info)?;
-        }
-        Ok(())
-    }
-
-    fn create_vkswapchainkhr(
-        device: &Device,
-        surface: vk::SurfaceKHR,
-        queue_families: &[u32],
-        image_extent: vk::Extent2D,
-        old_swapchain: Option<vk::SwapchainKHR>,
-        present_mode: vk::PresentModeKHR,
-        image_format: vk::Format,
-        image_color_space: vk::ColorSpaceKHR,
-        image_count: u32,
-    ) -> Result<(SwapchainHandle, Vec<vk::Image>)> {
-        let create_info = vk::SwapchainCreateInfoKHR::default()
-            .surface(surface)
-            .present_mode(present_mode)
-            .image_color_space(image_color_space)
-            .image_format(image_format)
-            .min_image_count(image_count)
-            .image_usage(vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::COLOR_ATTACHMENT)
-            .image_array_layers(1)
-            .image_extent(image_extent)
-            .image_sharing_mode(if queue_families.len() <= 1 {
-                vk::SharingMode::EXCLUSIVE
-            } else {
-                vk::SharingMode::CONCURRENT
-            })
-            .queue_family_indices(queue_families)
-            .pre_transform(vk::SurfaceTransformFlagsKHR::IDENTITY)
-            .composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE)
-            .old_swapchain(old_swapchain.unwrap_or(vk::SwapchainKHR::null()))
-            .clipped(true);
-
-        let (swapchain, images) = unsafe {
-            let swapchain = device.swapchain().create_swapchain(&create_info, None)?;
-
-            #[cfg(debug_assertions)]
-            {
-                let name = CString::new(format!(
-                    "swapchain-{}_{}",
-                    surface.as_raw(),
-                    SWAPCHAIN_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
-                ))
-                .unwrap();
-                _ = device.debug_utils().set_debug_utils_object_name(
-                    &vk::DebugUtilsObjectNameInfoEXT::default()
-                        .object_handle(swapchain)
-                        .object_name(&name),
-                );
-            }
-
-            let images = device.swapchain().get_swapchain_images(swapchain)?;
-
-            (SwapchainHandle::from_handle(swapchain), images)
-        };
-
-        Ok((swapchain, images))
-    }
-}
-static SWAPCHAIN_COUNT: AtomicU64 = AtomicU64::new(0);
-
-pub struct Surface {
-    instance: Arc<Instance>,
-    surface: vk::SurfaceKHR,
-}
-
-impl Debug for Surface {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("Surface")
-            .field("surface", &self.surface)
-            .finish()
-    }
-}
-
-impl Surface {
-    #[allow(dead_code)]
-    fn headless(instance: Arc<Instance>) -> Result<Self> {
-        unsafe {
-            let headless_instance =
-                ash::ext::headless_surface::Instance::new(&instance.entry, &instance.instance);
-
-            let surface = headless_instance
-                .create_headless_surface(&vk::HeadlessSurfaceCreateInfoEXT::default(), None)?;
-
-            Ok(Self { instance, surface })
-        }
-    }
-
-    fn create(
-        instance: Arc<Instance>,
-        display_handle: RawDisplayHandle,
-        window_handle: raw_window_handle::RawWindowHandle,
-    ) -> Result<Self> {
-        let surface = unsafe {
-            ash_window::create_surface(
-                &instance.entry,
-                &instance.instance,
-                display_handle,
-                window_handle,
-                None,
-            )?
-        };
-
-        Ok(Self { instance, surface })
-    }
-}
-
-impl Drop for Surface {
-    fn drop(&mut self) {
-        unsafe {
-            self.instance.surface.destroy_surface(self.surface, None);
-        }
-    }
-}
+pub mod swapchain;
 
 pub struct SamplerCache {
     device: Device,
@@ -1688,120 +1089,9 @@ impl Vulkan {
 
 use raw_window_handle::RawWindowHandle;
 
-pub struct WindowContext {
-    window_handle: RawWindowHandle,
-    surface: Arc<Surface>,
-    // this mutex is for guarding the swapchain against being replaced
-    // underneath WindowContext's functions
-    current_swapchain: RwLock<Arc<Swapchain>>,
-}
-
-impl Drop for WindowContext {
-    fn drop(&mut self) {
-        unsafe {
-            _ = self
-                .current_swapchain
-                .read()
-                .device
-                .dev()
-                .device_wait_idle();
-        }
-    }
-}
-
-unsafe impl Send for WindowContext {}
-unsafe impl Sync for WindowContext {}
-
-impl Borrow<RawWindowHandle> for WindowContext {
-    fn borrow(&self) -> &RawWindowHandle {
-        &self.window_handle
-    }
-}
-impl PartialEq for WindowContext {
-    fn eq(&self, other: &Self) -> bool {
-        self.window_handle == other.window_handle
-    }
-}
-impl Eq for WindowContext {}
-
-impl core::hash::Hash for WindowContext {
-    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-        self.window_handle.hash(state);
-    }
-}
-
-impl WindowContext {
-    fn new(
-        instance: Arc<Instance>,
-        device: Device,
-        extent: vk::Extent2D,
-        window_handle: raw_window_handle::RawWindowHandle,
-        display: RawDisplayHandle,
-    ) -> Result<Self> {
-        let surface = Arc::new(Surface::create(instance.clone(), display, window_handle)?);
-
-        let swapchain = Arc::new(Swapchain::new(
-            device.clone(),
-            surface.clone(),
-            device.phy(),
-            extent,
-        )?);
-
-        Ok(Self {
-            window_handle,
-            surface,
-            current_swapchain: RwLock::new(swapchain),
-        })
-    }
-
-    /// spawns a task that continuously requests images from the current
-    /// swapchain, sending them to a channel.  returns the receiver of the
-    /// channel, and a handle to the task, allowing for cancellation.
-    fn images(
-        self: Arc<Self>,
-    ) -> (
-        smol::channel::Receiver<SwapchainFrame>,
-        smol::Task<std::result::Result<(), Error>>,
-    ) {
-        let (tx, rx) = smol::channel::bounded(8);
-        let task = smol::spawn(async move {
-            loop {
-                let frame = self.acquire_image().await?;
-                tx.send(frame)
-                    .await
-                    .expect("channel closed on swapchain acquiring frame");
-            }
-        });
-
-        (rx, task)
-    }
-
-    async fn acquire_image(&self) -> Result<SwapchainFrame> {
-        // clone swapchain to keep it alive
-        let swapchain = self.current_swapchain.read().clone();
-        let (frame, suboptimal) = swapchain.clone().acquire_image().await?;
-        if suboptimal {
-            let mut lock = self.current_swapchain.write();
-            // only recreate our swapchain if it is still same, or else it might have already been recreated.
-            if Arc::ptr_eq(&swapchain, &lock) {
-                *lock = Arc::new(lock.recreate(None)?);
-            }
-        }
-
-        Ok(frame)
-    }
-
-    pub fn recreate_with(&self, extent: Option<vk::Extent2D>) -> Result<()> {
-        let mut swapchain = self.current_swapchain.write();
-        *swapchain = Arc::new(swapchain.recreate(extent)?);
-
-        Ok(())
-    }
-}
-
 #[derive(Debug)]
 pub struct EguiState {
-    textures: HashMap<egui::TextureId, EguiTextureInfo>,
+    pub textures: HashMap<egui::TextureId, EguiTextureInfo>,
     #[allow(unused)]
     descriptor_pool: pipeline::DescriptorPool,
     descriptor_set: vk::DescriptorSet,
@@ -1809,17 +1099,6 @@ pub struct EguiState {
     descriptor_layout: pipeline::DescriptorSetLayout,
     pipeline_layout: Arc<pipeline::PipelineLayout>,
     pipeline: Arc<pipeline::Pipeline>,
-    render_state: Option<EguiRenderState>,
-}
-
-#[derive(Debug)]
-struct EguiRenderState {
-    vertices: buffers::Buffer,
-    indices: buffers::Buffer,
-    draw_calls: buffers::Buffer,
-    texture_ids: buffers::Buffer,
-    textures_to_free: Vec<texture::TextureId>,
-    num_draw_calls: usize,
 }
 
 #[derive(Debug, Clone, Copy)]
@@ -1861,7 +1140,7 @@ impl EguiTextureInfo {
 impl EguiState {
     const TEXTURE_BINDING: u32 = 0;
     const UNIFORM_BINDING: u32 = 1;
-    fn new(device: Device) -> Result<Self> {
+    pub fn new(device: Device) -> Result<Self> {
         let (descriptor_pool, descriptor_layout, sets) =
             util::timed("Create Descriptor Set", || {
                 let descriptor_pool = pipeline::DescriptorPool::new(
@@ -2045,19 +1324,18 @@ impl EguiState {
             descriptor_set: sets[0],
             pipeline: Arc::new(pipeline),
             pipeline_layout: Arc::new(pipeline_layout),
-            render_state: None,
         })
     }
 
-    fn lookup_texture(&self, id: egui::epaint::TextureId) -> Option<texture::TextureId> {
+    pub fn lookup_texture(&self, id: egui::epaint::TextureId) -> Option<texture::TextureId> {
         self.textures.get(&id).map(|entry| entry.id)
     }
 }
 
 pub struct Renderer2 {
     device: Device,
-    samplers: SamplerCache,
-    texture_managers: texture::TextureManager,
+    pub samplers: SamplerCache,
+    pub texture_manager: texture::TextureManager,
 
     #[allow(unused)]
     // keep this around because the vulkan device has been created with this
@@ -2067,205 +1345,73 @@ pub struct Renderer2 {
 
 impl Renderer2 {
     pub fn new(display: RawDisplayHandle) -> Result<Self> {
-        let device = Device::new_from_default_desc(Some(display))?;
+        let device = Device::new_from_default_desc(Some(display), &[])?;
 
         Ok(Self {
             samplers: SamplerCache::new(device.clone()),
-            texture_managers: texture::TextureManager::new(device.clone()),
+            texture_manager: texture::TextureManager::new(device.clone()),
             display,
             device,
         })
     }
 
-    //pub fn create_surface
-}
-
-pub struct Renderer<W> {
-    pub texture_handler: texture::TextureManager,
-    pub egui_state: EguiState,
-    // thinkw: want renderer linked with display? then no (real) headless
-    display: RawDisplayHandle,
-    pub window_contexts: HashMap<W, WindowContext>,
-    pub vulkan: Vulkan,
-}
-
-pub use vk::Extent2D;
-
-impl<W> Renderer<W> {
-    pub fn new(display: RawDisplayHandle) -> Result<Self> {
-        let vulkan = Vulkan::new("Vidya", &[], &[], Some(display))?;
-        Ok(Self {
-            texture_handler: texture::TextureManager::new(vulkan.device.clone()),
-            egui_state: EguiState::new(vulkan.device.clone())?,
-            vulkan,
-            display,
-            window_contexts: HashMap::new(),
-        })
+    pub fn device(&self) -> &Device {
+        &self.device
     }
 
-    pub async fn draw_with_graph<K, F, G, T>(
+    pub fn samplers_mut(&mut self) -> &mut SamplerCache {
+        &mut self.samplers
+    }
+    pub fn samplers(&self) -> &SamplerCache {
+        &self.samplers
+    }
+    pub fn texture_manager_mut(&mut self) -> &mut texture::TextureManager {
+        &mut self.texture_manager
+    }
+    pub fn texture_manager(&self) -> &texture::TextureManager {
+        &self.texture_manager
+    }
+
+    pub fn create_surface(
+        &self,
+        window: RawWindowHandle,
+        extent: vk::Extent2D,
+    ) -> Result<swapchain::WindowSurface> {
+        swapchain::WindowSurface::new(self.device.clone(), extent, window, self.display)
+    }
+
+    pub async fn draw_graph<T, F: FnOnce(&mut Renderer2, &mut render_graph::RenderGraph) -> T>(
         &mut self,
-        window: &K,
-        pre_present_cb: F,
-        cb: G,
-    ) -> Result<T>
-    where
-        K: core::hash::Hash + Eq,
-        W: core::hash::Hash + Eq + Borrow<K>,
-        F: FnOnce(),
-        G: FnOnce(&mut Self, &mut render_graph::RenderGraph) -> T,
-    {
-        let dev = self.vulkan.device.clone();
-
-        let ctx = self.window_contexts.get(window).unwrap();
-        let (frame, suboptimal) = ctx.current_swapchain.read().clone().acquire_image().await?;
-
-        if suboptimal {
-            tracing::warn!(
-                "swapchain ({:?}) is suboptimal!",
-                ctx.current_swapchain.read().swapchain
-            );
-        }
-
-        let [r, g, b] =
-            rand::prelude::StdRng::seed_from_u64(ctx.surface.surface.as_raw()).gen::<[f32; 3]>();
-        let clear_color = Rgba([r, g, b, 1.0]);
+        surface: &swapchain::WindowSurface,
+        cb: F,
+    ) -> Result<T> {
+        let frame = surface.acquire_image().await?;
 
         let mut rg = render_graph::RenderGraph::new();
-        let framebuffer = rg.import_image(frame.image.clone(), Access::undefined());
-        rg.mark_as_output(framebuffer, Access::present());
-        rg.framebuffer = Some(framebuffer);
+        let framebuffer = rg.import_framebuffer(frame.image.clone());
 
-        render_graph::clear_pass(&mut rg, clear_color, framebuffer);
+        let out = cb(self, &mut rg);
 
-        let t = cb(self, &mut rg);
-
-        let cmds = rg.resolve(dev.clone())?;
+        let cmds = rg.resolve(self.device.clone())?;
 
         let future = cmds.submit(
             Some((frame.acquire, vk::PipelineStageFlags::TRANSFER)),
             Some(frame.release),
-            Arc::new(sync::Fence::create(dev.clone())?),
+            Arc::new(sync::Fence::create(self.device.clone())?),
         )?;
 
-        // call pre_present_notify
-        pre_present_cb();
-
         future.await;
+
+        // window.window.pre_present_notify();
         let wait = Some(frame.release);
         frame.present(wait)?;
 
-        Ok(t)
-    }
-
-    pub fn debug_draw_egui<K, F>(
-        &mut self,
-        window: &K,
-        egui_ctx: &egui::Context,
-        output: egui::FullOutput,
-        pre_present_cb: F,
-    ) -> Result<()>
-    where
-        K: core::hash::Hash + Eq,
-        W: core::hash::Hash + Eq + Borrow<K>,
-        F: FnOnce(),
-    {
-        let dev = self.vulkan.device.clone();
-
-        if let Some(ctx) = self.window_contexts.get(window) {
-            let (frame, suboptimal) =
-                smol::block_on(ctx.current_swapchain.read().clone().acquire_image())?;
-
-            if suboptimal {
-                tracing::warn!(
-                    "swapchain ({:?}) is suboptimal!",
-                    ctx.current_swapchain.read().swapchain
-                );
-            }
-
-            let [r, g, b] = rand::prelude::StdRng::seed_from_u64(ctx.surface.surface.as_raw())
-                .gen::<[f32; 3]>();
-            let clear_color = Rgba([r, g, b, 1.0]);
-
-            let mut rg = render_graph::RenderGraph::new();
-            let (textures_to_remove, cmds) = util::timed("record command buffer", || {
-                let framebuffer = rg.import_image(frame.image.clone(), Access::undefined());
-
-                render_graph::clear_pass(&mut rg, clear_color, framebuffer);
-                egui_pass::egui_pre_pass(
-                    &dev,
-                    &mut rg,
-                    &mut self.texture_handler,
-                    &mut self.egui_state,
-                    &output,
-                )?;
-
-                let textures_to_remove = egui_pass::egui_pass(
-                    &dev,
-                    &mut rg,
-                    &mut self.texture_handler,
-                    &mut self.vulkan.samplers,
-                    &mut self.egui_state,
-                    egui_ctx,
-                    output,
-                    framebuffer,
-                )?;
-
-                rg.mark_as_output(framebuffer, Access::present());
-
-                Result::Ok((textures_to_remove, rg.resolve(dev.clone())?))
-            })?;
-
-            let future = cmds.submit(
-                Some((frame.acquire, vk::PipelineStageFlags::TRANSFER)),
-                Some(frame.release),
-                Arc::new(sync::Fence::create(dev.clone())?),
-            )?;
-
-            // call pre_present_notify
-            pre_present_cb();
-
-            let wait = Some(frame.release);
-            frame.present(wait)?;
-            future.block()?;
-
-            for id in textures_to_remove {
-                self.texture_handler.remove_texture(id);
-            }
-        }
-
-        Ok(())
-    }
-
-    pub fn new_window_context(
-        &mut self,
-        extent: vk::Extent2D,
-        window_id: W,
-        window: raw_window_handle::WindowHandle,
-    ) -> Result<()>
-    where
-        W: core::hash::Hash + Eq,
-    {
-        use std::collections::hash_map::Entry;
-        match self.window_contexts.entry(window_id) {
-            Entry::Vacant(entry) => {
-                let ctx = WindowContext::new(
-                    self.vulkan.instance.clone(),
-                    self.vulkan.device.clone(),
-                    extent,
-                    window.as_raw(),
-                    self.display,
-                )?;
-
-                entry.insert(ctx);
-            }
-            _ => {}
-        }
-
-        Ok(())
+        Ok(out)
     }
 }
 
+pub use vk::Extent2D;
+
 mod debug {
     use ash::vk;
     use tracing::{event, Level};
@@ -2852,58 +1998,3 @@ pub mod utils {
         }
     }
 }
-
-#[cfg(test)]
-mod test_swapchain {
-    use super::*;
-
-    fn create_headless_vk() -> Result<(Vulkan, WindowContext)> {
-        let vk = Vulkan::new(
-            "testing",
-            &[],
-            &[ash::ext::headless_surface::NAME, khr::surface::NAME],
-            None,
-        )?;
-
-        let surface = Arc::new(Surface::headless(vk.instance.clone())?);
-
-        let swapchain = Arc::new(Swapchain::new(
-            vk.device.clone(),
-            surface.clone(),
-            vk.device.phy(),
-            vk::Extent2D::default().width(1).height(1),
-        )?);
-
-        let window_ctx = WindowContext {
-            window_handle: RawWindowHandle::Web(raw_window_handle::WebWindowHandle::new(0)),
-            surface,
-            current_swapchain: RwLock::new(swapchain),
-        };
-
-        Ok((vk, window_ctx))
-    }
-
-    #[tracing_test::traced_test]
-    #[test]
-    fn async_swapchain_acquiring() {
-        let (_vlk, ctx) = create_headless_vk().expect("init");
-        let ctx = Arc::new(ctx);
-        let (rx, handle) = ctx.clone().images();
-
-        eprintln!("hello world!");
-
-        let mut count = 0;
-        loop {
-            let now = std::time::Instant::now();
-            let frame = rx.recv_blocking().expect("recv");
-
-            _ = frame.present(None);
-            tracing::info!("mspf: {:.3}ms", now.elapsed().as_micros() as f32 / 1e3);
-            count += 1;
-            if count > 1000 {
-                smol::block_on(handle.cancel());
-                break;
-            }
-        }
-    }
-}
diff --git a/crates/renderer/src/swapchain.rs b/crates/renderer/src/swapchain.rs
new file mode 100644
index 0000000..b014eb4
--- /dev/null
+++ b/crates/renderer/src/swapchain.rs
@@ -0,0 +1,763 @@
+use std::{
+    marker::PhantomData,
+    sync::{
+        atomic::{AtomicU32, AtomicU64},
+        Arc,
+    },
+};
+
+use ash::{
+    prelude::VkResult,
+    vk::{self, Handle},
+};
+use parking_lot::{RawMutex, RwLock};
+use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
+
+use crate::{
+    define_device_owned_handle,
+    device::{Device, DeviceOwned},
+    images, sync,
+    util::RawMutexGuard,
+    Instance, Result,
+};
+
+define_device_owned_handle! {
+    #[derive(Debug)]
+    pub Surface(vk::SurfaceKHR) {
+    } => |this| unsafe {
+        this.device().instance().surface.destroy_surface(this.handle(), None);
+    }
+}
+
+impl Surface {
+    #[allow(dead_code)]
+    pub fn headless(device: Device) -> Result<Self> {
+        unsafe {
+            let instance = device.instance();
+            let headless_instance =
+                ash::ext::headless_surface::Instance::new(&instance.entry, &instance.instance);
+
+            let surface = headless_instance
+                .create_headless_surface(&vk::HeadlessSurfaceCreateInfoEXT::default(), None)?;
+
+            Ok(Self::construct(
+                device,
+                surface,
+                Some("headless-surface".into()),
+            )?)
+        }
+    }
+
+    pub fn create(
+        device: Device,
+        display_handle: RawDisplayHandle,
+        window_handle: raw_window_handle::RawWindowHandle,
+    ) -> Result<Self> {
+        let instance = device.instance();
+        let surface = unsafe {
+            ash_window::create_surface(
+                &instance.entry,
+                &instance.instance,
+                display_handle,
+                window_handle,
+                None,
+            )?
+        };
+
+        Ok(Self::construct(
+            device,
+            surface,
+            Some(format!("{:?}_surface", window_handle).into()),
+        )?)
+    }
+}
+
+define_device_owned_handle! {
+    pub Swapchain(vk::SwapchainKHR) {
+        mutex: RawMutex,
+        surface: Arc<Surface>,
+
+        #[allow(unused)]
+        present_mode: vk::PresentModeKHR,
+        #[allow(unused)]
+        color_space: vk::ColorSpaceKHR,
+        format: vk::Format,
+        images: Vec<Arc<images::Image>>,
+        image_views: Vec<vk::ImageView>,
+        extent: vk::Extent2D,
+        min_image_count: u32,
+
+        // sync objects:
+        // we need two semaphores per each image, one acquire-semaphore and one release-semaphore.
+        // semaphores must be unique to each frame and cannot be reused per swapchain.
+        acquire_semaphores: Vec<vk::Semaphore>,
+        release_semaphores: Vec<vk::Semaphore>,
+
+        // one fence per in-flight frame, to synchronize image acquisition
+        fences: Vec<Arc<sync::Fence>>,
+
+        current_frame: AtomicU32,
+
+        // for khr_present_id/khr_present_wait
+        #[allow(unused)]
+        present_id: AtomicU64,
+    } => |this| unsafe {
+        _ = this.device().wait_queue_idle(this.device().present_queue());
+        tracing::debug!("dropping swapchain {:?}", this.handle());
+        for view in &this.image_views {
+            this.device().dev().destroy_image_view(*view, None);
+        }
+
+        this.with_locked(|swapchain| {
+            this.device().swapchain().destroy_swapchain(swapchain, None)
+        });
+
+        for &semaphore in this
+            .acquire_semaphores
+            .iter()
+            .chain(&this.release_semaphores)
+        {
+            this.device().dev().destroy_semaphore(semaphore, None);
+        }
+    }
+}
+
+impl core::fmt::Debug for Swapchain {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Swapchain")
+            .field("inner", &self.inner)
+            .field("surface", &self.surface)
+            .field("present_mode", &self.present_mode)
+            .field("color_space", &self.color_space)
+            .field("format", &self.format)
+            .field("images", &self.images)
+            .field("image_views", &self.image_views)
+            .field("extent", &self.extent)
+            .field("min_image_count", &self.min_image_count)
+            .field("acquire_semaphores", &self.acquire_semaphores)
+            .field("release_semaphores", &self.release_semaphores)
+            .field("fences", &self.fences)
+            .field("current_frame", &self.current_frame)
+            .field("present_id", &self.present_id)
+            .finish()
+    }
+}
+
+impl Swapchain {
+    const PREFERRED_IMAGES_IN_FLIGHT: u32 = 3;
+
+    fn get_swapchain_params_from_surface(
+        instance: &Arc<Instance>,
+        surface: vk::SurfaceKHR,
+        pdev: vk::PhysicalDevice,
+        requested_extent: Option<vk::Extent2D>,
+    ) -> Result<SwapchainParams> {
+        let caps = unsafe {
+            instance
+                .surface
+                .get_physical_device_surface_capabilities(pdev, surface)?
+        };
+        let formats = unsafe {
+            instance
+                .surface
+                .get_physical_device_surface_formats(pdev, surface)?
+        };
+        let present_modes = unsafe {
+            instance
+                .surface
+                .get_physical_device_surface_present_modes(pdev, surface)?
+        };
+
+        let present_mode = present_modes
+            .iter()
+            .find(|&mode| mode == &vk::PresentModeKHR::MAILBOX)
+            .cloned()
+            .unwrap_or(vk::PresentModeKHR::FIFO);
+
+        let format = formats
+            .iter()
+            .max_by_key(|&&format| {
+                let is_rgba_unorm = format.format == vk::Format::R8G8B8A8_UNORM
+                    || format.format == vk::Format::B8G8R8A8_UNORM;
+                let is_srgb = format.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR;
+                is_rgba_unorm as u8 * 10 + is_srgb as u8
+            })
+            .or(formats.first())
+            .cloned()
+            .expect("no surface format available!");
+
+        // 0 here means no limit
+        let max_image_count = core::num::NonZero::new(caps.max_image_count)
+            .map(|n| n.get())
+            .unwrap_or(u32::MAX);
+
+        // we want PREFERRED_IMAGES_IN_FLIGHT images acquired at the same time,
+        let image_count =
+            (caps.min_image_count + Self::PREFERRED_IMAGES_IN_FLIGHT).min(max_image_count);
+
+        let extent = current_extent_or_clamped(
+            &caps,
+            requested_extent.unwrap_or(vk::Extent2D::default().width(1).height(1)),
+        );
+
+        Ok(SwapchainParams {
+            present_mode,
+            format: format.format,
+            color_space: format.color_space,
+            image_count,
+            extent,
+            min_image_count: caps.min_image_count,
+        })
+    }
+
+    pub fn new(
+        device: Device,
+        surface: Arc<Surface>,
+        pdev: vk::PhysicalDevice,
+        extent: vk::Extent2D,
+    ) -> Result<Self> {
+        Self::create(device, surface, pdev, Some(extent), None)
+    }
+
+    fn create(
+        device: Device,
+        surface: Arc<Surface>,
+        pdev: vk::PhysicalDevice,
+        extent: Option<vk::Extent2D>,
+        old_swapchain: Option<&Self>,
+    ) -> Result<Self> {
+        let SwapchainParams {
+            present_mode,
+            format,
+            color_space,
+            image_count,
+            min_image_count,
+            extent,
+        } = Self::get_swapchain_params_from_surface(
+            device.instance(),
+            surface.handle(),
+            pdev,
+            extent,
+        )?;
+
+        let (swapchain, images) = {
+            let lock = old_swapchain.as_ref().map(|handle| handle.lock());
+
+            Self::create_vkswapchainkhr(
+                &device,
+                surface.handle(),
+                &device.queue_families().swapchain_family_indices(),
+                extent,
+                lock.as_ref().map(|lock| **lock),
+                present_mode,
+                format,
+                color_space,
+                image_count,
+            )
+        }?;
+
+        let images = images
+            .iter()
+            .enumerate()
+            .map(|(i, image)| unsafe {
+                images::Image::from_swapchain_image(
+                    device.clone(),
+                    *image,
+                    Some(format!("swapchain-{:x}-image-{i}", swapchain.as_raw()).into()),
+                    vk::Extent3D {
+                        width: extent.width,
+                        height: extent.height,
+                        depth: 1,
+                    },
+                    format,
+                )
+                .inspect(|img| {
+                    _ = img.get_view(images::ImageViewDesc {
+                        // TODO: make this a function that uses debug name/surface handle
+                        name: Some(
+                            format!("swapchain-{:x}-image-{i}-view", swapchain.as_raw()).into(),
+                        ),
+                        kind: vk::ImageViewType::TYPE_2D,
+                        format,
+                        aspect: vk::ImageAspectFlags::COLOR,
+                        ..Default::default()
+                    });
+                })
+                .map(|img| Arc::new(img))
+            })
+            .collect::<VkResult<Vec<_>>>()?;
+
+        let image_views = images
+            .iter()
+            .enumerate()
+            .map(|(i, image)| {
+                image.get_view(images::ImageViewDesc {
+                    name: Some(format!("swapchain-{:x}-image-{i}-view", swapchain.as_raw()).into()),
+                    kind: vk::ImageViewType::TYPE_2D,
+                    format,
+                    aspect: vk::ImageAspectFlags::COLOR,
+                    ..Default::default()
+                })
+            })
+            .collect::<VkResult<Vec<_>>>()?;
+
+        let num_images = images.len() as u32;
+        let inflight_frames = num_images - min_image_count;
+
+        let acquire_semaphores = {
+            (0..inflight_frames)
+                .map(|i| unsafe {
+                    device
+                        .dev()
+                        .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)
+                        .inspect(|r| {
+                            #[cfg(debug_assertions)]
+                            {
+                                device
+                                    .debug_name_object(
+                                        *r,
+                                        &format!("semaphore-{:x}_{i}-acquire", swapchain.as_raw()),
+                                    )
+                                    .unwrap();
+                            }
+                        })
+                })
+                .collect::<VkResult<Vec<_>>>()?
+        };
+
+        let release_semaphores = {
+            (0..inflight_frames)
+                .map(|i| unsafe {
+                    device
+                        .dev()
+                        .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)
+                        .inspect(|r| {
+                            #[cfg(debug_assertions)]
+                            {
+                                device
+                                    .debug_name_object(
+                                        *r,
+                                        &format!("semaphore-{:x}_{i}-release", swapchain.as_raw()),
+                                    )
+                                    .unwrap();
+                            }
+                        })
+                })
+                .collect::<VkResult<Vec<_>>>()?
+        };
+
+        let fences = {
+            (0..inflight_frames)
+                .map(|i| {
+                    Ok(Arc::new(sync::Fence::create(device.clone()).inspect(
+                        |r| {
+                            #[cfg(debug_assertions)]
+                            {
+                                device
+                                    .debug_name_object(
+                                        r.fence(),
+                                        &format!("fence-{:x}_{i}", swapchain.as_raw()),
+                                    )
+                                    .unwrap();
+                            }
+                        },
+                    )?))
+                })
+                .collect::<VkResult<Vec<_>>>()?
+        };
+
+        tracing::trace!("fences: {fences:?}");
+
+        Ok(Self::construct(
+            device,
+            swapchain,
+            Some(
+                format!(
+                    "swapchain-{}_{}",
+                    surface.handle().as_raw(),
+                    SWAPCHAIN_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
+                )
+                .into(),
+            ),
+            <parking_lot::RawMutex as parking_lot::lock_api::RawMutex>::INIT,
+            surface,
+            present_mode,
+            color_space,
+            format,
+            images,
+            image_views,
+            extent,
+            min_image_count,
+            acquire_semaphores,
+            release_semaphores,
+            fences,
+            AtomicU32::new(0),
+            AtomicU64::new(1),
+        )?)
+    }
+
+    pub fn max_in_flight_images(&self) -> u32 {
+        self.num_images() - self.min_image_count
+    }
+
+    pub fn num_images(&self) -> u32 {
+        self.images.len() as u32
+    }
+
+    fn recreate(&self, extent: Option<vk::Extent2D>) -> Result<Self> {
+        Self::create(
+            self.device().clone(),
+            self.surface.clone(),
+            self.device().phy(),
+            extent,
+            Some(&self),
+        )
+    }
+
+    /// returns a future yielding the frame, and true if the swapchain is
+    /// suboptimal and should be recreated.
+    fn acquire_image(
+        self: Arc<Self>,
+    ) -> impl std::future::Future<Output = VkResult<(SwapchainFrame, bool)>> {
+        let frame = self
+            .current_frame
+            .fetch_update(
+                std::sync::atomic::Ordering::Release,
+                std::sync::atomic::Ordering::Relaxed,
+                |i| Some((i + 1) % self.max_in_flight_images()),
+            )
+            .unwrap() as usize;
+
+        tracing::trace!(frame, "acquiring image for frame {frame}");
+
+        async move {
+            let fence = self.fences[frame].clone();
+            let acquire = self.acquire_semaphores[frame];
+            let release = self.release_semaphores[frame];
+
+            // spawn on threadpool because it might block.
+            let (idx, suboptimal) = smol::unblock({
+                let this = self.clone();
+                let fence = fence.clone();
+                move || unsafe {
+                    this.with_locked(|swapchain| {
+                        this.device().swapchain().acquire_next_image(
+                            swapchain,
+                            u64::MAX,
+                            acquire,
+                            fence.fence(),
+                        )
+                    })
+                }
+            })
+            .await?;
+
+            // wait for image to become available.
+            sync::FenceFuture::new(fence.clone()).await;
+
+            let idx = idx as usize;
+            let image = self.images[idx].clone();
+            let view = self.image_views[idx];
+
+            Ok((
+                SwapchainFrame {
+                    index: idx as u32,
+                    swapchain: self.clone(),
+                    format: self.format,
+                    image,
+                    view,
+                    acquire,
+                    release,
+                },
+                suboptimal,
+            ))
+        }
+    }
+
+    fn present(&self, frame: SwapchainFrame, wait: Option<vk::Semaphore>) -> Result<()> {
+        let swpchain = self.lock();
+        let queue = self.device().present_queue().lock();
+
+        let wait_semaphores = wait
+            .as_ref()
+            .map(|sema| core::slice::from_ref(sema))
+            .unwrap_or_default();
+
+        // TODO: make this optional for devices with no support for present_wait/present_id
+        // let present_id = self
+        //     .present_id
+        //     .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
+        // let mut present_id =
+        //     vk::PresentIdKHR::default().present_ids(core::slice::from_ref(&present_id));
+
+        let present_info = vk::PresentInfoKHR::default()
+            .image_indices(core::slice::from_ref(&frame.index))
+            .swapchains(core::slice::from_ref(&swpchain))
+            .wait_semaphores(wait_semaphores);
+        //.push_next(&mut present_id)
+
+        // call winits pre_present_notify here
+
+        unsafe {
+            self.device()
+                .swapchain()
+                .queue_present(*queue, &present_info)?;
+        }
+        Ok(())
+    }
+
+    fn create_vkswapchainkhr(
+        device: &Device,
+        surface: vk::SurfaceKHR,
+        queue_families: &[u32],
+        image_extent: vk::Extent2D,
+        old_swapchain: Option<vk::SwapchainKHR>,
+        present_mode: vk::PresentModeKHR,
+        image_format: vk::Format,
+        image_color_space: vk::ColorSpaceKHR,
+        image_count: u32,
+    ) -> Result<(vk::SwapchainKHR, Vec<vk::Image>)> {
+        let create_info = vk::SwapchainCreateInfoKHR::default()
+            .surface(surface)
+            .present_mode(present_mode)
+            .image_color_space(image_color_space)
+            .image_format(image_format)
+            .min_image_count(image_count)
+            .image_usage(vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::COLOR_ATTACHMENT)
+            .image_array_layers(1)
+            .image_extent(image_extent)
+            .image_sharing_mode(if queue_families.len() <= 1 {
+                vk::SharingMode::EXCLUSIVE
+            } else {
+                vk::SharingMode::CONCURRENT
+            })
+            .queue_family_indices(queue_families)
+            .pre_transform(vk::SurfaceTransformFlagsKHR::IDENTITY)
+            .composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE)
+            .old_swapchain(old_swapchain.unwrap_or(vk::SwapchainKHR::null()))
+            .clipped(true);
+
+        let (swapchain, images) = unsafe {
+            let swapchain = device.swapchain().create_swapchain(&create_info, None)?;
+
+            let images = device.swapchain().get_swapchain_images(swapchain)?;
+
+            (swapchain, images)
+        };
+
+        Ok((swapchain, images))
+    }
+}
+static SWAPCHAIN_COUNT: AtomicU64 = AtomicU64::new(0);
+
+#[derive(Debug)]
+#[must_use = "This struct represents an acquired image from the swapchain and
+ must be presented in order to free resources on the device."]
+pub struct SwapchainFrame {
+    pub swapchain: Arc<Swapchain>,
+    pub index: u32,
+    pub image: Arc<images::Image>,
+    pub format: vk::Format,
+    pub view: vk::ImageView,
+    pub acquire: vk::Semaphore,
+    pub release: vk::Semaphore,
+}
+
+impl Eq for SwapchainFrame {}
+impl PartialEq for SwapchainFrame {
+    fn eq(&self, other: &Self) -> bool {
+        self.index == other.index && self.image == other.image
+    }
+}
+
+impl SwapchainFrame {
+    pub fn present(self, wait: Option<vk::Semaphore>) -> crate::Result<()> {
+        self.swapchain.clone().present(self, wait)
+    }
+}
+
+fn current_extent_or_clamped(
+    caps: &vk::SurfaceCapabilitiesKHR,
+    fallback: vk::Extent2D,
+) -> vk::Extent2D {
+    if caps.current_extent.width == u32::MAX {
+        vk::Extent2D {
+            width: fallback
+                .width
+                .clamp(caps.min_image_extent.width, caps.max_image_extent.width),
+            height: fallback
+                .height
+                .clamp(caps.min_image_extent.height, caps.max_image_extent.height),
+        }
+    } else {
+        caps.current_extent
+    }
+}
+
+struct SwapchainParams {
+    present_mode: vk::PresentModeKHR,
+    format: vk::Format,
+    color_space: vk::ColorSpaceKHR,
+    /// the number of images to request from the device
+    image_count: u32,
+    /// the minimum number of images the surface permits
+    min_image_count: u32,
+    extent: vk::Extent2D,
+}
+
+impl Swapchain {
+    pub fn lock(&self) -> RawMutexGuard<'_, vk::SwapchainKHR> {
+        use parking_lot::lock_api::RawMutex;
+        self.mutex.lock();
+        RawMutexGuard {
+            mutex: &self.mutex,
+            value: &self.inner.object,
+            _pd: PhantomData,
+        }
+    }
+
+    pub fn with_locked<T, F: FnOnce(vk::SwapchainKHR) -> T>(&self, f: F) -> T {
+        let lock = self.lock();
+        f(*lock)
+    }
+}
+
+#[derive(Debug)]
+pub struct WindowSurface {
+    // window_handle: RawWindowHandle,
+    pub surface: Arc<Surface>,
+    // this mutex is for guarding the swapchain against being replaced
+    // underneath WindowContext's functions
+    current_swapchain: RwLock<Arc<Swapchain>>,
+}
+
+impl WindowSurface {
+    pub fn new(
+        device: Device,
+        requested_extent: vk::Extent2D,
+        window: RawWindowHandle,
+        display: RawDisplayHandle,
+    ) -> Result<Self> {
+        let surface = Arc::new(Surface::create(device.clone(), display, window)?);
+        let swapchain = RwLock::new(Arc::new(Swapchain::new(
+            device.clone(),
+            surface.clone(),
+            device.phy(),
+            requested_extent,
+        )?));
+
+        Ok(Self {
+            surface,
+            // window_handle: window,
+            current_swapchain: swapchain,
+        })
+    }
+
+    /// spawns a task that continuously requests images from the current
+    /// swapchain, sending them to a channel.  returns the receiver of the
+    /// channel, and a handle to the task, allowing for cancellation.
+    pub fn images(
+        self: Arc<Self>,
+    ) -> (
+        smol::channel::Receiver<SwapchainFrame>,
+        smol::Task<std::result::Result<(), crate::Error>>,
+    ) {
+        let (tx, rx) = smol::channel::bounded(8);
+        let task = smol::spawn(async move {
+            loop {
+                let frame = self.acquire_image().await?;
+                tx.send(frame)
+                    .await
+                    .expect("channel closed on swapchain acquiring frame");
+            }
+        });
+
+        (rx, task)
+    }
+
+    pub async fn acquire_image(&self) -> Result<SwapchainFrame> {
+        // clone swapchain to keep it alive
+        let swapchain = self.current_swapchain.read().clone();
+        let (frame, suboptimal) = swapchain.clone().acquire_image().await?;
+        if suboptimal {
+            let mut lock = self.current_swapchain.write();
+            // only recreate our swapchain if it is still same, or else it might have already been recreated.
+            if Arc::ptr_eq(&swapchain, &lock) {
+                *lock = Arc::new(lock.recreate(None)?);
+            }
+        }
+
+        Ok(frame)
+    }
+
+    pub fn recreate_with(&self, extent: Option<vk::Extent2D>) -> Result<()> {
+        let mut swapchain = self.current_swapchain.write();
+        *swapchain = Arc::new(swapchain.recreate(extent)?);
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::device;
+
+    use super::*;
+
+    fn create_headless_vk() -> Result<(Device, WindowSurface)> {
+        let device = Device::new_from_default_desc(
+            None,
+            &[
+                device::Extension {
+                    name: "VK_EXT_headless_surface",
+                    version: ash::ext::headless_surface::SPEC_VERSION,
+                },
+                device::Extension {
+                    name: "VK_KHR_surface",
+                    version: ash::khr::surface::SPEC_VERSION,
+                },
+            ],
+        )?;
+
+        let surface = Arc::new(Surface::headless(device.clone())?);
+
+        let swapchain = Arc::new(Swapchain::new(
+            device.clone(),
+            surface.clone(),
+            device.phy(),
+            vk::Extent2D::default().width(1).height(1),
+        )?);
+
+        let window_ctx = WindowSurface {
+            // window_handle: RawWindowHandle::Web(raw_window_handle::WebWindowHandle::new(0)),
+            surface,
+            current_swapchain: RwLock::new(swapchain),
+        };
+
+        Ok((device, window_ctx))
+    }
+
+    #[tracing_test::traced_test]
+    #[test]
+    fn async_swapchain_acquiring() {
+        let (_dev, ctx) = create_headless_vk().expect("init");
+        let ctx = Arc::new(ctx);
+        let (rx, handle) = ctx.clone().images();
+
+        eprintln!("hello world!");
+
+        let mut count = 0;
+        loop {
+            let now = std::time::Instant::now();
+            let frame = rx.recv_blocking().expect("recv");
+
+            _ = frame.present(None);
+            tracing::info!("mspf: {:.3}ms", now.elapsed().as_micros() as f32 / 1e3);
+            count += 1;
+            if count > 1000 {
+                smol::block_on(handle.cancel());
+                break;
+            }
+        }
+    }
+}