Я продолжаю публиковать переводы руководства к Vulkan API (cсылка на оригинал — vulkan-tutorial.com), и сегодня хочу поделиться переводом новой главы — Swap chain из раздела Drawing a triangle, подраздела Presentation.
image(VkImage)
, в который будет рисовать, а после отрисовки отправляет его обратно в очередь. То, каким именно образом работает очередь, зависит от настроек, но основная задача swap chain – синхронизировать вывод изображений с частотой обновления экрана. VK_KHR_swapchain
.isDeviceSuitable
, чтобы проверить, поддерживается ли расширение. Ранее мы уже работали со списком поддерживаемых расширений, поэтому сложностей возникнуть не должно. Обратите внимание, что заголовочный файл Vulkan предоставляет удобный макрос VK_KHR_SWAPCHAIN_EXTENSION_NAME
, который определен как «VK_KHR_swapchain
». Преимущество этого макроса в том, что, если вы допустите ошибку в написании, компилятор вас об этом предупредит.const std::vector<const char*> deviceExtensions = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
checkDeviceExtensionSupport
, вызываемую из isDeviceSuitable
:bool isDeviceSuitable(VkPhysicalDevice device) {
QueueFamilyIndices indices = findQueueFamilies(device);
bool extensionsSupported = checkDeviceExtensionSupport(device);
return indices.isComplete() && extensionsSupported;
}
bool checkDeviceExtensionSupport(VkPhysicalDevice device) {
return true;
}
bool checkDeviceExtensionSupport(VkPhysicalDevice device) {
uint32_t extensionCount;
vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr);
std::vector<VkExtensionProperties> availableExtensions(extensionCount);
vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, availableExtensions.data());
std::set<std::string> requiredExtensions(deviceExtensions.begin(), deviceExtensions.end());
for (const auto& extension : availableExtensions) {
requiredExtensions.erase(extension.extensionName);
}
return requiredExtensions.empty();
}
std::set<std::string>
, чтобы хранить имена требуемых, но еще не подтвержденных расширений. Вы также можете использовать вложенный цикл, как в функции checkValidationLayerSupport
. Разница в производительности не существенна. VK_KHR_swapchain
. Для этого немного изменим заполнение VkDeviceCreateInfo
при создании логического устройства:createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
struct SwapChainSupportDetails {
VkSurfaceCapabilitiesKHR capabilities;
std::vector<VkSurfaceFormatKHR> formats;
std::vector<VkPresentModeKHR> presentModes;
};
querySwapChainSupport
, которая заполняет эту структуру.SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) {
SwapChainSupportDetails details;
return details;
}
VkSurfaceCapabilitiesKHR
. vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities);
VkPhysicalDevice
и VkSurfaceKHR
. Каждый раз, когда мы будем запрашивать поддерживаемый функционал, эти два параметра будут первыми, поскольку они являются ключевыми компонентами swap chain. uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);
if (formatCount != 0) {
details.formats.resize(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data());
}
vkGetPhysicalDeviceSurfacePresentModesKHR
:uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr);
if (presentModeCount != 0) {
details.presentModes.resize(presentModeCount);
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data());
}
isDeviceSuitable
, чтобы проверить, поддерживается ли swap chain. В рамках этого руководства, будем считать, что если есть хотя бы один поддерживаемый формат изображений и один поддерживаемый режим работы для window surface, значит swap chain поддерживается.bool swapChainAdequate = false;
if (extensionsSupported) {
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty();
}
return indices.isComplete() && extensionsSupported && swapChainAdequate;
swapChainAdequate
имеет значение true, значит swap chain поддерживается. Но у swap chain может быть несколько режимов. Напишем несколько функций, чтобы подобрать подходящие настройки для создания наиболее эффективной swap chain. VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) {
}
formats
из структуры SwapChainSupportDetails
в качестве аргумента.availableFormats
содержит члены format
и colorSpace
. Поле format
определяет количество и типы каналов. Например, VK_FORMAT_B8G8R8A8_SRGB
обозначает, что у нас есть B, G, R и альфа каналы по 8 бит, всего 32 бита на пиксель. С помощью флага VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
в поле colorSpace
указывается, поддерживается ли цветовое пространство SRGB. Обратите внимание, что в ранней версии спецификации этот флаг назывался VK_COLORSPACE_SRGB_NONLINEAR_KHR
.VK_FORMAT_B8G8R8A8_SRGB
. for (const auto& availableFormat : availableFormats) {
if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
return availableFormat;
}
}
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];
}
VK_PRESENT_MODE_IMMEDIATE_KHR
: изображения, отправленные вашим приложением, немедленно отправляются на экран, что может приводить к артефактам.VK_PRESENT_MODE_FIFO_KHR
: изображения для вывода на экран берутся из начала очереди в момент обновления экрана. В то время, как программа помещает отрендеренные изображения в конец очереди. Если очередь заполнена, программа будет ждать. Это похоже на вертикальную синхронизацию, используемую в современных играх.VK_PRESENT_MODE_FIFO_RELAXED_KHR
: этот режим отличается от предыдущего только в одном случае, когда происходит задержка программы и в момент обновления экрана остается пустая очередь. Тогда изображение передается на экран сразу после его появления без ожидания обновления экрана. Это может привести к видимым артефактам.VK_PRESENT_MODE_MAILBOX_KHR
: это еще один вариант второго режима. Вместо того, чтобы блокировать программу при заполнении очереди, изображения в очереди заменяются новыми. Этот режим подходит для реализации тройной буферизации. С ней вы можете избежать появления артефактов при низком времени ожидания.VK_PRESENT_MODE_FIFO_KHR
, поэтому нам снова придется написать функцию для поиска лучшего доступного режима:VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) {
return 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;
}
VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {
}
VkSurfaceCapabilitiesKHR
. Vulkan сообщает нам, какое разрешение мы должны выставить, с помощью поля currentExtent
(соответствует размеру окна). Однако некоторые оконные менеджеры допускают использование разных разрешений. Для этого указывается специальное значение ширины и высоты в currentExtent
— максимальное значение типа uint32_t
. В таком случае из промежутка между minImageExtent
и maxImageExtent
мы выберем разрешение, которое больше всего соответствует разрешению окна. Главное — правильно указать единицы измерения.{WIDTH, HEIGHT}
, которое мы указали при создании окна, измеряется в экранных координатах. Но поскольку Vulkan работает с пикселями, разрешение swap chain тоже должно быть указано в пикселях. Если вы используете дисплей с высоким разрешением (например, дисплей Retina от Apple), экранные координаты не соответствуют пикселям: из-за более высокой плотности пикселей разрешение окна в пикселях выше, чем в экранных координатах. Так как Vulkan сам не исправит разрешение swap chain для нас, мы не можем использовать исходное разрешение {WIDTH, HEIGHT}
. Вместо этого мы должны использовать glfwGetFramebufferSize
, чтобы запросить разрешение окна в пикселях, прежде чем сопоставлять его с минимальным и максимальным разрешением изображений.#include <cstdint> // Necessary for UINT32_MAX
...
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::max(capabilities.minImageExtent.width, std::min(capabilities.maxImageExtent.width, actualExtent.width));
actualExtent.height = std::max(capabilities.minImageExtent.height, std::min(capabilities.maxImageExtent.height, actualExtent.height));
return actualExtent;
}
}
max
и min
здесь используются для ограничения значений width
и height
в пределах доступных разрешений. Не забудьте подключить заголовочный файл <algorithm>
для использования функций.createSwapChain
и вызовем ее из initVulkan
после создания логического устройства.void initVulkan() {
createInstance();
setupDebugMessenger();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapChain();
}
void createSwapChain() {
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice);
VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats);
VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes);
VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities);
}
uint32_t imageCount = swapChainSupport.capabilities.minImageCount;
uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;
0
обозначает, что максимум не задан.if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) {
imageCount = swapChainSupport.capabilities.maxImageCount;
}
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;
imageArrayLayers
указывается число слоев, из которых состоит каждый image. Здесь всегда будет значение 1
, если, конечно, это не стереоизображения. Битовое поле imageUsage
указывает, для каких операций будут использоваться images, полученные из swap chain. В руководстве мы будем рендерить непосредственно в них, но вы можете сначала рендерить в отдельный image, например, для постобработки. В таком случае используйте значение VK_IMAGE_USAGE_TRANSFER_DST_BIT
, а для переноса используйте операции перемещения в памяти (memory operation).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
}
VK_SHARING_MODE_EXCLUSIVE
: объект принадлежит одному семейству очередей, и право владения должно быть передано явно перед использованием его в другом семействе очередей. Такой способ обеспечивает самую высокую производительность.VK_SHARING_MODE_CONCURRENT
: объекты могут использоваться в нескольких семействах очередей без явной передачи права владения.VK_SHARING_MODE_CONCURRENT
. Для этого способа требуется заранее указать, между какими семействами очередей будет разделено владение. Это можно сделать с помощью параметров queueFamilyIndexCount
и pQueueFamilyIndices
. Если семейство графических очередей и семейство очередей отображения совпадают, что случается чаще, используйте VK_SHARING_MODE_EXCLUSIVE
. createInfo.preTransform = swapChainSupport.capabilities.currentTransform;
supportedTransforms
в capabilities
), например, поворот на 90 градусов по часовой стрелке или отражение по горизонтали. Чтобы не применять никаких преобразований, просто оставьте currentTransform
.createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
compositeAlpha
указывает, нужно ли использовать альфа-канал для смешивания с другими окнами в оконной системе. Скорее всего, альфа-канал вам не понадобится, поэтому оставьте VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR
.createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;
presentMode
говорит само за себя. Если мы выставим VK_TRUE
в поле clipped
, значит нас не интересуют скрытые пикселы (например, если часть нашего окна перекрыта другим окном). Вы всегда сможете выключить clipping, если вам понадобится прочитать пиксели, а пока оставим clipping включенным.createInfo.oldSwapchain = VK_NULL_HANDLE;
oldSwapChain
. Если swap chain станет недействительной, например, из-за изменения размера окна, ее нужно будет воссоздать с нуля и в поле oldSwapChain
указать ссылку на старую swap chain. Это сложная тема, которую мы рассмотрим в одной из следующих глав. Пока представим, что у нас будет только одна swap chain.VkSwapchainKHR
:VkSwapchainKHR swapChain;
vkCreateSwapchainKHR
для создания swap chain: if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) {
throw std::runtime_error("failed to create swap chain!");
}
vkDestroySwapchainKHR
до уничтожения устройства:void cleanup() {
vkDestroySwapchainKHR(device, swapChain, nullptr);
...
}
«Не удалось найти vkGetInstanceProcAddress в SteamOverlayVulkanLayer.dll»
, зайдите в раздел FAQ.createInfo.imageExtent = extent;
с включенными слоями валидации. Один из уровней валидации сразу же обнаружит ошибку и уведомит нас:std::vector<VkImage> swapChainImages;
vkCreateSwapchainKHR
добавим код для получения дескрипторов. Помните, что мы указали только минимальное количество изображений в swap chain, это значит, что их может быть и больше. Поэтому сначала запросим реальное количество изображений с помощью функции vkGetSwapchainImagesKHR
, затем выделим необходимое место в контейнере и снова вызовем vkGetSwapchainImagesKHR
для получения дескрипторов.vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data());
VkSwapchainKHR swapChain;
std::vector<VkImage> swapChainImages;
VkFormat swapChainImageFormat;
VkExtent2D swapChainExtent;
...
swapChainImageFormat = surfaceFormat.format;
swapChainExtent = extent;
К сожалению, не доступен сервер mySQL