"""Swapchain creation and recreation."""
from __future__ import annotations
import logging
from typing import Any
import vulkan as vk
__all__ = ["Swapchain"]
log = logging.getLogger(__name__)
_PREFERRED_SRGB_FORMATS = {
vk.VK_FORMAT_B8G8R8A8_SRGB,
vk.VK_FORMAT_R8G8B8A8_SRGB,
}
_ACCEPTABLE_UNORM_FORMATS = {
vk.VK_FORMAT_B8G8R8A8_UNORM,
vk.VK_FORMAT_R8G8B8A8_UNORM,
}
def _choose_surface_format(formats: list[Any]) -> Any:
"""Pick the best surface format. Prefers SRGB, accepts UNORM, falls back to first available."""
# Prefer any SRGB format with SRGB nonlinear colour space
for f in formats:
if f.format in _PREFERRED_SRGB_FORMATS and f.colorSpace == vk.VK_COLOR_SPACE_SRGB_NONLINEAR_KHR:
return f
# Accept UNORM with SRGB colour space (common on Android)
for f in formats:
if f.format in _ACCEPTABLE_UNORM_FORMATS and f.colorSpace == vk.VK_COLOR_SPACE_SRGB_NONLINEAR_KHR:
return f
return formats[0]
def _choose_present_mode(modes: list[int], *, vsync: bool = False) -> int:
if vsync:
return vk.VK_PRESENT_MODE_FIFO_KHR
if vk.VK_PRESENT_MODE_MAILBOX_KHR in modes:
return vk.VK_PRESENT_MODE_MAILBOX_KHR
return vk.VK_PRESENT_MODE_FIFO_KHR
def _clamp_extent(capabilities: Any, desired: tuple[int, int]) -> Any:
if capabilities.currentExtent.width != 0xFFFFFFFF:
return capabilities.currentExtent
min_e, max_e = capabilities.minImageExtent, capabilities.maxImageExtent
w = max(min_e.width, min(max_e.width, desired[0]))
h = max(min_e.height, min(max_e.height, desired[1]))
return vk.VkExtent2D(width=w, height=h)
[docs]
class Swapchain:
"""Manages VkSwapchainKHR lifecycle."""
def __init__(
self,
instance: Any,
device: Any,
physical_device: Any,
surface: Any,
extent: tuple[int, int],
graphics_family: int,
present_family: int,
*,
vsync: bool = False,
) -> None:
self.device = device
self.physical_device = physical_device
self.surface = surface
self.extent = extent
self.graphics_family = graphics_family
self.present_family = present_family
self._vsync = vsync
self.handle: Any = None
self.images: list[Any] = []
self.image_views: list[Any] = []
self.image_format: int = 0
# Load KHR extension functions
self._get_capabilities = vk.vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceSurfaceCapabilitiesKHR")
self._get_formats = vk.vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceSurfaceFormatsKHR")
self._get_present_modes = vk.vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceSurfacePresentModesKHR")
self._create_swapchain = vk.vkGetInstanceProcAddr(instance, "vkCreateSwapchainKHR")
self._get_images = vk.vkGetInstanceProcAddr(instance, "vkGetSwapchainImagesKHR")
self._destroy_swapchain = vk.vkGetInstanceProcAddr(instance, "vkDestroySwapchainKHR")
[docs]
def create(self) -> None:
capabilities = self._get_capabilities(self.physical_device, self.surface)
formats = self._get_formats(self.physical_device, self.surface)
present_modes = self._get_present_modes(self.physical_device, self.surface)
surface_format = _choose_surface_format(formats)
present_mode = _choose_present_mode(present_modes, vsync=self._vsync)
extent = _clamp_extent(capabilities, self.extent)
image_count = capabilities.minImageCount + 1
if capabilities.maxImageCount > 0:
image_count = min(image_count, capabilities.maxImageCount)
families = [self.graphics_family, self.present_family]
sharing = vk.VK_SHARING_MODE_EXCLUSIVE
if self.graphics_family != self.present_family:
sharing = vk.VK_SHARING_MODE_CONCURRENT
create_info = vk.VkSwapchainCreateInfoKHR(
surface=self.surface,
minImageCount=image_count,
imageFormat=surface_format.format,
imageColorSpace=surface_format.colorSpace,
imageExtent=extent,
imageArrayLayers=1,
imageUsage=vk.VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | vk.VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
imageSharingMode=sharing,
queueFamilyIndexCount=len(families) if sharing == vk.VK_SHARING_MODE_CONCURRENT else 0,
pQueueFamilyIndices=families if sharing == vk.VK_SHARING_MODE_CONCURRENT else None,
preTransform=capabilities.currentTransform,
compositeAlpha=vk.VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
presentMode=present_mode,
clipped=vk.VK_TRUE,
oldSwapchain=self.handle,
)
self.handle = self._create_swapchain(self.device, create_info, None)
self.image_format = surface_format.format
self.images = self._get_images(self.device, self.handle)
self.extent = (extent.width, extent.height)
self.image_views = []
for img in self.images:
view_info = vk.VkImageViewCreateInfo(
image=img,
viewType=vk.VK_IMAGE_VIEW_TYPE_2D,
format=self.image_format,
components=vk.VkComponentMapping(
r=vk.VK_COMPONENT_SWIZZLE_IDENTITY,
g=vk.VK_COMPONENT_SWIZZLE_IDENTITY,
b=vk.VK_COMPONENT_SWIZZLE_IDENTITY,
a=vk.VK_COMPONENT_SWIZZLE_IDENTITY,
),
subresourceRange=vk.VkImageSubresourceRange(
aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT,
baseMipLevel=0,
levelCount=1,
baseArrayLayer=0,
layerCount=1,
),
)
self.image_views.append(vk.vkCreateImageView(self.device, view_info, None))
log.debug("Swapchain created: %dx%d, %d images", *self.extent, len(self.images))
[docs]
def recreate(self, extent: tuple[int, int]) -> None:
vk.vkDeviceWaitIdle(self.device)
self._destroy_views()
old = self.handle
self.extent = extent
self.create()
if old:
self._destroy_swapchain(self.device, old, None)
def _destroy_views(self) -> None:
for view in self.image_views:
vk.vkDestroyImageView(self.device, view, None)
self.image_views.clear()
[docs]
def destroy(self) -> None:
self._destroy_views()
if self.handle:
self._destroy_swapchain(self.device, self.handle, None)
self.handle = None