Source code for simvx.graphics.gpu.swapchain

"""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