swap chain可以直接理解为等待渲染到屏幕上的图片队列。具体的工作方式和swap chain的设置方式有关,但是一般都是将图像的显示和屏幕的刷新率同步。

swap chain检测

并非所有的显卡都支持直接将图像显示在屏幕上,例如服务器端的显卡。并且由于图像的展示和所在平台的窗口系统联系很紧密,而vulkan又是跨平台的,所以这并不是vulkan的核心组件,需要检测VK_KHR_swapchain扩展并启用。

swap chain扩展检测

这里的检测用于检测物理设备,像前面检测实例扩展很类似

// 要使用的swap chain扩展
const std::vector<const char*> deviceExtensions = {
  VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
 
bool checkDeviceExtensionSupport(VkPhysicalDevice device) {
	uint32_t extensionCount;
	vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount,
										 nullptr);
	std::vector<VkExtensionProperties> availableExtensions(extensionCount);
	vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount,
										 availableExtensions.data());
	
	for (auto& extension : deviceExtensions) {
	  bool found = false;
	  for (const auto& availableExtension : availableExtensions) {
		if (strcmp(extension, availableExtension.extensionName) == 0) {
		  found = true;
		  break;
		}
	  }
	  if (!found) {
		return false;
	  }
	}
	return true;
}

window surface对swap chain的支持能力检测

仅仅只是检查物理设备是否支持swap chain还不够,还需要检查其是否和window surface兼容。还需要检测

  • 基本的surface能力
  • surface格式
  • 展示模式
// 封装了一下swap chain要检测的信息
struct SwapChainSupportDetails {
	VkSurfaceCapabilitiesKHR capabilities;
	std::vector<VkSurfaceFormatKHR> formats;
	std::vector<VkPresentModeKHR> presentModes;
};
 
SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) {
	// surface的能力
	SwapChainSupportDetails details;
	vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface,
											  &details.capabilities);
	// surface的格式
	uint32_t formatCount;
	vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount,
										 nullptr);
	if (formatCount != 0) {
	  details.formats.resize(formatCount);
	  vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount,
										   details.formats.data());
	}
	// surface的呈现模式
	uint32_t presentModeCount;
	vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface,
											  &presentModeCount, nullptr);
	if (presentModeCount != 0) {
	  details.presentModes.resize(presentModeCount);
	  vkGetPhysicalDeviceSurfacePresentModesKHR(
		  device, surface, &presentModeCount, details.presentModes.data());
	}
	return details;
}
 
// 设备的选择再加上一个检测条件
bool isDeviceSuitable(VkPhysicalDevice device) {
	QueueFamilyIndices indices = findQueueFamilies(device);
	// 设备加上swap chain的扩展检查
	bool extensionsSupported = checkDeviceExtensionSupport(device);
	bool swapChainAdequate = false;
	if (extensionsSupported) {
	  SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
	  swapChainAdequate = !swapChainSupport.formats.empty() &&
						  !swapChainSupport.presentModes.empty();
	}
	
	return indices.isComplete() && extensionsSupported && swapChainAdequate;
}

启动扩展

就是填充createInfo,注意是逻辑设备的createInfo结构体。我们只是在物理设备上进行检测,但是最终还是使用逻辑设备和队列来和物理设备交互的。

createInfo.enabledExtensionCount =
        static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();

设置swap chain

window surface对swap chain有很多设置,通过检测的时候我们也获取到了surface对swap chain的支持情况。现在就是选择最适合的swap chain设置。

其实也就是前面检测的三种类型参数。

surface format

这里设置图像的格式和色彩空间。

  • 格式推荐VK_FORMAT_B8G8R8A8_SRGB
  • 色彩空间推荐VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
VkSurfaceFormatKHR chooseSwapSurfaceFormat(
  const std::vector<VkSurfaceFormatKHR>& availableFormats) {
	for (const auto& availableFormat : availableFormats) {
	  if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB &&
		  availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
		return availableFormat;
	  }
	}
	return availableFormats[0];
}

显示模式

这是swap chain最重要的部分,vulkan有下面四种可用的模式

  • VK_PRESENT_MODE_IMMEDIATE_KHR:提交的图片立即展示到屏幕上,可能导致画面撕裂
  • VK_PRESENT_MODE_FIFO_KHR:swap chain是一个队列,屏幕从队列当中取出图像,程序提交的图片放在队列的尾部。队列要是满了,那么程序就必须阻塞,与现代游戏当中的垂直同步类似,屏幕刷新的那一刻叫做垂直空白。
  • VK_PRESENT_MODE_FIFO_RELAXED_KHR:与前者的不同在于,当swap chain为空之后,应用提交的图像会立即被传输到显示上而不是等待下一个垂直空白。这回造成画面撕裂。
  • VK_PRESENT_MODE_MAILBOX_KHR:第二种方式另一个变体。swap chain队列满了不会阻塞应用程序则是将队列中的图像替换成图像。保证尽可能渲染新图像同时避免撕裂。可以比标准的垂直同步延迟更小。这通常被称为“三重缓冲”,尽管仅存在三个缓冲液,并不一定意味着帧率已解锁。

其中VK_PRESENT_MODE_FIFO_KHR是保证支持的。在检测其他模式的支持情况的时候,VK_PRESENT_MODE_MAILBOX_KHR是更加推荐的,但是在移动平台上更新注重能效,因此优先使用VK_PRESENT_MODE_FIFO_KHR。

那么这里的选择逻辑就很清晰了

VkPresentModeKHR chooseSwapPresentMode(
  const std::vector<VkPresentModeKHR>& availablePresentModes) {
	for (const auto& availablePresentMode : availablePresentModes) {
	  if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) {
		return availablePresentMode;
	  }
	}
	return VK_PRESENT_MODE_FIFO_KHR;
}

Swap extent

这个东西可以简单理解为图像的分辨率。通常设置成显示器相同。再简单点就是设置宽度和高度。

  VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {
    if (capabilities.currentExtent.width != UINT32_MAX) {
      return capabilities.currentExtent;
    } else {
      int width, height;
      // 这里选择创建的窗口的宽和高,也就是填充窗口
      glfwGetFramebufferSize(window, &width, &height);
      VkExtent2D actualExtent = {static_cast<uint32_t>(width),
                                 static_cast<uint32_t>(height)};
    // 设置了最大值和最小值
      actualExtent.width =
          std::clamp(actualExtent.width, capabilities.minImageExtent.width,
                     capabilities.maxImageExtent.width);
      actualExtent.height =
          std::clamp(actualExtent.height, capabilities.minImageExtent.height,
                     capabilities.maxImageExtent.height);
      return actualExtent;
    }
  }

创建swap chain

基于上面查询到的swap chain的支持情况,以及选择的推荐配置项,创建swap chain

VkSwapchainKHR swapChain;
 
  void createSwapChain() {
    SwapChainSupportDetails swapChainSupport =
        querySwapChainSupport(physicalDevice);
 
    VkSurfaceFormatKHR surfaceFormat =
        chooseSwapSurfaceFormat(swapChainSupport.formats);
    VkPresentModeKHR presentMode =
        chooseSwapPresentMode(swapChainSupport.presentModes);
    VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities);
    // 设置交换链的图像数量,图像数量最小为capabilities.minImageCount+1,避免等待
    // 交换链的图像数量capabilities.maxImageCount ==
    // 0,说明交换链的图像数量不受限制
    uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;
    if (swapChainSupport.capabilities.maxImageCount > 0 &&
        imageCount > swapChainSupport.capabilities.maxImageCount) {
      imageCount = swapChainSupport.capabilities.maxImageCount;
    }
    // 填充createInfo
    VkSwapchainCreateInfoKHR createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
    createInfo.surface = surface;
    createInfo.minImageCount = imageCount;
    createInfo.imageFormat = surfaceFormat.format;
    createInfo.imageColorSpace = surfaceFormat.colorSpace;
    createInfo.imageExtent = extent;
    createInfo.imageArrayLayers = 1;
    createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
    // graphics和present队列不同,需要设置共享模式来避免拥有权的转移。这里设置为并发共享模式只是为了方便
    QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
    uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(),
                                     indices.presentFamily.value()};
 
    if (indices.graphicsFamily != indices.presentFamily) {
      createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
      createInfo.queueFamilyIndexCount = 2;
      createInfo.pQueueFamilyIndices = queueFamilyIndices;
    } else {
      createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
      createInfo.queueFamilyIndexCount = 0;      // Optional
      createInfo.pQueueFamilyIndices = nullptr;  // Optional
    }
    createInfo.preTransform = swapChainSupport.capabilities.currentTransform;
    createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
    createInfo.presentMode = presentMode;
    createInfo.clipped = VK_TRUE;
    createInfo.oldSwapchain = VK_NULL_HANDLE;
    if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) !=
        VK_SUCCESS) {
      throw std::runtime_error("failed to create swap chain!");
    }
  }

在应用退出之前进行销毁

vkDestroySwapchainKHR(device, swapChain, nullptr);

获取swap chain中的图像

拿到swap chain当中的图像,方便后续处理。这里的数据会在swap chain销毁的时候也自动销毁,无需手动清理

std::vector<VkImage> swapChainImages;
 
// 在上面的createSwapChain函数尾部添加下面代码
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapChain, &imageCount,
						swapChainImages.data());

再存储一下前面创建swap chain设置的属性,图像格式和分辨率,方便后续处理

VkFormat swapChainImageFormat; 
VkExtent2D swapChainExtent;
 
swapChainImageFormat = surfaceFormat.format; 
swapChainExtent = extent;

创建ImageView

为了swap chain中的每个VkImage都创建对应的ImageView

std::vector<VkImageView> swapChainImageViews;
swapChainImageViews.resize(swapChainImages.size());
for (size_t i = 0; i < swapChainImages.size(); i++) {
  VkImageViewCreateInfo createInfo{};
  createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
  createInfo.image = swapChainImages[i];
  createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
  createInfo.format = swapChainImageFormat;
  createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
  createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
  createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
  createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
  createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
  createInfo.subresourceRange.baseMipLevel = 0;
  createInfo.subresourceRange.levelCount = 1;
  createInfo.subresourceRange.baseArrayLayer = 0;
  createInfo.subresourceRange.layerCount = 1;
  if (vkCreateImageView(device, &createInfo, nullptr,
						&swapChainImageViews[i]) != VK_SUCCESS) {
	throw std::runtime_error("failed to create image views!");
  }
}

ImageView不像Image是swap chain自动创建和释放的,我们手动创建的资源当然也要手动释放

for (auto imageView : swapChainImageViews) {
  vkDestroyImageView(device, imageView, nullptr);
}