vidya/crates/renderer/src/swapchain.rs
2026-03-31 16:56:05 +02:00

839 lines
29 KiB
Rust

use std::{
collections::HashMap,
marker::PhantomData,
sync::{
Arc,
atomic::{AtomicU32, AtomicU64, Ordering},
},
};
use ash::{
khr,
prelude::VkResult,
vk::{self, Handle},
};
use parking_lot::{Mutex, RawMutex, RwLock};
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
use crate::{
Instance, PhysicalDeviceInfo, Result, SurfaceCapabilities, define_device_owned_handle,
device::{Device, DeviceObject, DeviceOwned},
images,
instance::InstanceInner,
sync,
util::RawMutexGuard,
};
use derive_more::Debug;
#[derive(Debug)]
pub struct Surface {
pub(crate) raw: vk::SurfaceKHR,
#[debug(skip)]
pub(crate) functor: khr::surface::Instance,
pub(crate) instance: Instance,
pub(crate) capabilities: Mutex<HashMap<vk::PhysicalDevice, SurfaceCapabilities>>,
pub(crate) swapchain: RwLock<Option<Arc<Swapchain>>>,
}
impl Drop for Surface {
fn drop(&mut self) {
unsafe {
self.functor.destroy_surface(self.raw, None);
}
}
}
impl Surface {
pub fn get_capabilities<'a>(
&'a self,
adapter: vk::PhysicalDevice,
) -> parking_lot::MappedMutexGuard<'a, SurfaceCapabilities> {
let lock = self.capabilities.lock();
parking_lot::MutexGuard::map(
lock,
|caps: &mut HashMap<vk::PhysicalDevice, SurfaceCapabilities>| {
caps.entry(adapter).or_insert_with(|| {
self.instance
.get_adapter_surface_capabilities(adapter, self)
.expect("surface is not compatible with the adapter")
})
},
)
}
pub fn configure(&self, device: &Device, config: SwapchainConfiguration) -> Result<()> {
let guard = self.swapchain.read();
if let Some(swapchain) = guard.as_ref()
&& swapchain.config.eq(&config)
&& swapchain.swapchain.device() == device
{
// the current swapchain already matches the requested configuration, so we can skip reconfiguration.
return Ok(());
}
let new_swapchain = Swapchain::new(
device.clone(),
self,
config,
guard.as_ref().map(AsRef::as_ref),
)?;
drop(guard);
self.swapchain.write().replace(Arc::new(new_swapchain));
todo!()
}
#[allow(dead_code)]
pub fn headless(instance: &Instance) -> Result<Self> {
let headless_instance =
ash::ext::headless_surface::Instance::new(&instance.entry, &instance.raw);
let functor = khr::surface::Instance::new(&instance.entry, &instance.raw);
// SAFETY: the headless surface does not have any platform-specific requirements, and does not depend on any external handles, so it is safe to create without additional guarantees.
// (note): ash marks this function as unsafe, likely because of
// auto-generated bindings from vk.xml, but doesn't provide any safety
// bounds.
unsafe {
let raw = headless_instance
.create_headless_surface(&vk::HeadlessSurfaceCreateInfoEXT::default(), None)?;
Ok(Self {
raw,
functor,
capabilities: Mutex::new(HashMap::new()),
swapchain: RwLock::new(None),
instance: instance.clone(),
})
}
}
/// # Safety
///
/// The caller must ensure that the provided display and window handles are
/// valid and remain valid for the lifetime of the surface. Namely, the
/// window handle must refer to a valid window that is associated with the
/// display handle, and both must not be destroyed while the surface is
/// still in use. Additionally, the caller must ensure that the instance
/// was created with the appropriate platform-specific surface extensions
/// enabled.
pub unsafe fn new_from_raw_window_handle(
instance: &Instance,
display_handle: RawDisplayHandle,
window_handle: RawWindowHandle,
) -> Result<Self> {
let functor = khr::surface::Instance::new(&instance.entry, &instance.raw);
// SAFETY: the caller guarantees the validity of the display and window handles, and that they remain valid for the lifetime of the surface.
let raw = unsafe {
ash_window::create_surface(
&instance.entry,
&instance.raw,
display_handle,
window_handle,
None,
)?
};
Ok(Self {
raw,
functor,
capabilities: Mutex::new(HashMap::new()),
swapchain: RwLock::new(None),
instance: instance.clone(),
})
}
/// Validates a swapchain configuration and possibly adjusts it to be
/// compatible with the surface capabilities by setting incompatible fields
/// to default fallbacks.
fn validate_swapchain_configuration(
&self,
instance: &Instance,
adapter: &PhysicalDeviceInfo,
config: &mut SwapchainConfiguration,
) -> Result<()> {
let surface_caps = instance.get_adapter_surface_capabilities(adapter.pdev, self)?;
let max_image_dim = adapter.properties.core.limits.max_image_dimension2_d;
if config.extent.width > max_image_dim || config.extent.height > max_image_dim {
return Err(crate::Error::ImageTooLarge {
width: config.extent.width,
height: config.extent.height,
max_size: max_image_dim,
});
}
if config.extent.width == 0 || config.extent.height == 0 {
return Err(crate::Error::ImageZeroSized);
}
if !surface_caps.present_modes.contains(&config.present_mode) {
// find the first of these modes that is supported by the surface, in order of preference.
let fallback_modes = [
vk::PresentModeKHR::MAILBOX,
vk::PresentModeKHR::IMMEDIATE,
vk::PresentModeKHR::FIFO,
];
let fallback_mode = fallback_modes
.iter()
.find(|&&mode| surface_caps.present_modes.contains(&mode))
.cloned().expect("FIFO is guaranteed to be supported as per Vulkan spec, so this should never happen");
config.present_mode = fallback_mode;
}
if !surface_caps.formats.iter().any(|&format| {
format.format == config.format && format.color_space == config.color_space
}) {
// wgpu just rejects the swapchain if the format is not supported. is that smarter?
// find a fallback format
let format = surface_caps
.formats
.iter()
.max_by_key(|&&format| {
// prefer UNORM RGBA formats, and then SRGB color space
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
})
// fall back to the first available format
.or(surface_caps.formats.first())
.cloned()
.expect("no surface format available!");
config.format = format.format;
config.color_space = format.color_space;
}
Ok(())
}
fn get_fallback_swapchain_configuration(
&self,
instance: &Instance,
adapter: &PhysicalDeviceInfo,
) -> Result<SwapchainConfiguration> {
let surface_caps = instance.get_adapter_surface_capabilities(adapter.pdev, self)?;
let present_mode = surface_caps
.present_modes
.iter()
.find(|&mode| mode == &vk::PresentModeKHR::MAILBOX)
.cloned()
.unwrap_or(vk::PresentModeKHR::FIFO);
let format = surface_caps
.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(surface_caps.formats.first())
.cloned()
.expect("no surface format available!");
// 0 here means no limit
let max_image_count = core::num::NonZero::new(surface_caps.capabilities.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 = (surface_caps.capabilities.min_image_count
+ Swapchain::PREFERRED_IMAGES_IN_FLIGHT)
.min(max_image_count);
let extent = current_extent_or_clamped(
&surface_caps.capabilities,
vk::Extent2D::default().width(1).height(1),
);
let composite_alpha_mode = if surface_caps
.capabilities
.supported_composite_alpha
.contains(vk::CompositeAlphaFlagsKHR::OPAQUE)
{
vk::CompositeAlphaFlagsKHR::OPAQUE
} else {
// if the surface doesn't support opaque alpha, we can still use inherit, which means the alpha will be determined by the window system. This is supported by all window systems.
vk::CompositeAlphaFlagsKHR::INHERIT
};
Ok(SwapchainConfiguration {
present_mode,
format: format.format,
color_space: format.color_space,
image_count,
extent,
usage: vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::COLOR_ATTACHMENT,
composite_alpha_mode,
})
}
}
#[derive(Debug)]
pub struct Swapchain {
// swapchain images, managed by the swapchain and must not be destroyed manually.
images: Vec<vk::Image>,
swapchain: DeviceObject<vk::SwapchainKHR>,
// this carries the device handle, however the `swapchain` field holds a ref count on the device, so it is safe to hold the pointer in the functor as well.
#[debug(skip)]
functor: khr::swapchain::Device,
/// current configuration of the swapchain.
config: SwapchainConfiguration,
/// the minimum number of images the surface permits. This is used to calculate how many images we can have in-flight at the same time.
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<vk::Fence>,
current_frame: AtomicU32,
// Some of the swapchain operations require external synchronisation; this mutex allows `Swapchain` to be `Sync`.
#[debug(skip)]
guard: parking_lot::RawMutex,
// for khr_present_id/khr_present_wait
#[allow(unused)]
present_id: AtomicU64,
}
impl Swapchain {
/// This function frees the manually managed objects associated with the swapchain.
/// This function MUST be called once and only once before the swapchain is dropped.
pub unsafe fn release_resources(&self) {
_ = self.swapchain.device().wait_idle();
for fence in &self.fences {
unsafe {
self.swapchain.device().raw.destroy_fence(*fence, None);
}
}
for &semaphore in self
.acquire_semaphores
.iter()
.chain(&self.release_semaphores)
{
unsafe {
self.swapchain
.device()
.raw
.destroy_semaphore(semaphore, None);
}
}
}
}
impl Drop for Swapchain {
fn drop(&mut self) {
unsafe {
self.release_resources();
self.functor.destroy_swapchain(*self.swapchain, None);
}
todo!()
}
}
impl Swapchain {
const PREFERRED_IMAGES_IN_FLIGHT: u32 = 3;
fn new(
device: Device,
surface: &Surface,
mut config: SwapchainConfiguration,
old_swapchain: Option<&Self>,
) -> Result<Self> {
surface.validate_swapchain_configuration(&device.instance, &device.adapter, &mut config)?;
let surface_caps = device
.instance
.get_adapter_surface_capabilities(device.adapter.pdev, &surface)?;
let functor = device
.device_extensions
.swapchain
.clone()
.expect("swapchain extension not loaded");
let (swapchain, images) = {
let _lock = old_swapchain.as_ref().map(|old| old.lock());
let old_swapchain = old_swapchain
.map(|swp| *swp.swapchain)
.unwrap_or(vk::SwapchainKHR::null());
let queue_families = device.queues.swapchain_family_indices();
let create_info = vk::SwapchainCreateInfoKHR::default()
.surface(surface.raw)
.present_mode(config.present_mode)
.image_color_space(config.color_space)
.image_format(config.format)
.min_image_count(surface_caps.capabilities.min_image_count)
.image_usage(config.usage)
.image_array_layers(1)
.image_extent(config.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(config.composite_alpha_mode)
.old_swapchain(old_swapchain)
.clipped(true);
let (swapchain, images) = unsafe {
let swapchain = functor.create_swapchain(&create_info, None)?;
let images = functor.get_swapchain_images(swapchain)?;
(swapchain, images)
};
(swapchain, images)
};
let num_images = images.len() as u32;
let inflight_frames = num_images - surface_caps.capabilities.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()),
);
})
})
.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()),
);
})
})
.collect::<VkResult<Vec<_>>>()?
};
let fences = {
(0..inflight_frames)
.map(|i| unsafe {
let fence = device
.raw
.create_fence(&vk::FenceCreateInfo::default(), None)?;
device.debug_name_object(fence, &format!("fence-{:x}_{i}", swapchain.as_raw()));
Ok(fence)
})
.collect::<VkResult<Vec<_>>>()?
};
tracing::trace!("fences: {fences:?}");
Ok(Swapchain {
functor: device
.device_extensions
.swapchain
.clone()
.expect("swapchain extension not loaded"),
swapchain: DeviceObject::new(
swapchain,
device,
Some(format!("swapchain-{:x}", swapchain.as_raw()).into()),
),
images,
config,
guard: <RawMutex as parking_lot::lock_api::RawMutex>::INIT,
min_image_count: surface_caps.capabilities.min_image_count,
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
}
/// 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
.try_update(Ordering::Release, 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];
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();
move || unsafe {
this.with_locked(|swapchain| {
this.functor
.acquire_next_image(swapchain, u64::MAX, acquire, 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.config.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_slice();
// 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.functor.queue_present(*queue, &present_info)?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn create_raw(
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 swapchain_loader = device
.device_extensions
.swapchain
.as_ref()
.expect("swapchain extension not loaded");
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 = swapchain_loader.create_swapchain(&create_info, None)?;
let images = swapchain_loader.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
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SwapchainConfiguration {
pub present_mode: vk::PresentModeKHR,
pub format: vk::Format,
pub color_space: vk::ColorSpaceKHR,
/// the number of images to request from the device
pub image_count: u32,
/// The dimensions of the swapchain images.
pub extent: vk::Extent2D,
/// Alpha compositing mode.
pub composite_alpha_mode: vk::CompositeAlphaFlagsKHR,
/// Usage flags for the swapchain images. This should be a combination of
/// `vk::ImageUsageFlags::COLOR_ATTACHMENT` and
/// `vk::ImageUsageFlags::TRANSFER_DST`, but can include additional usage
/// flags if supported by the device and surface.
pub usage: vk::ImageUsageFlags,
}
impl Swapchain {
pub fn lock(&self) -> RawMutexGuard<'_, vk::SwapchainKHR> {
use parking_lot::lock_api::RawMutex;
self.guard.lock();
RawMutexGuard {
mutex: &self.guard,
value: &*self.swapchain,
_pd: PhantomData,
}
}
pub fn with_locked<T, F: FnOnce(vk::SwapchainKHR) -> T>(&self, f: F) -> T {
let lock = self.lock();
f(*lock)
}
}
// impl WindowSurface {
// pub fn new(
// device: Device,
// requested_extent: vk::Extent2D,
// window: RawWindowHandle,
// display: RawDisplayHandle,
// ) -> Result<Self> {
// let surface = Arc::new(unsafe {
// Surface::new_from_raw_window_handle(device.instance(), 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 acquire_image_blocking(&self) -> Result<SwapchainFrame> {
// smol::block_on(self.acquire_image())
// }
// 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::{PhysicalDeviceFeatures, instance::InstanceDesc, make_extension};
use super::*;
fn create_headless_vk() -> Result<(Device, Arc<Surface>)> {
let instance = Instance::new(&InstanceDesc {
instance_extensions: &[
make_extension!(ash::ext::headless_surface),
make_extension!(ash::khr::surface),
],
..Default::default()
})?;
let features = PhysicalDeviceFeatures {
core13: vk::PhysicalDeviceVulkan13Features {
synchronization2: vk::TRUE,
dynamic_rendering: vk::TRUE,
maintenance4: vk::TRUE,
..Default::default()
},
..Default::default()
};
let surface = Arc::new(Surface::headless(&instance)?);
let adapter = instance.choose_adapter_default(Some(&surface), &[], Some(&features))?;
let device = adapter.create_logical_device(&instance, &[], features, None)?;
surface.configure(
&device,
SwapchainConfiguration {
present_mode: vk::PresentModeKHR::FIFO,
format: vk::Format::R8G8B8A8_UNORM,
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR,
image_count: 2,
extent: vk::Extent2D::default().width(1).height(1),
composite_alpha_mode: vk::CompositeAlphaFlagsKHR::OPAQUE,
usage: vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::COLOR_ATTACHMENT,
},
)?;
Ok((device, surface))
}
#[tracing_test::traced_test]
#[test]
fn async_swapchain_acquiring() {
let (_dev, surface) = create_headless_vk().expect("init");
let ctx = Arc::new(surface);
// 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;
// }
// }
}
}