Vulkan Tutorial 2 presentation

目录

5 窗口表面

6 交换链

7 图像视图


5 窗口表面

由于Vulkan是一个与平台无关的API,它自己不能直接与窗口系统对接。为了在Vulkan和窗口系统之间建立连接,将结果呈现在屏幕上,我们需要使用WSI(窗口系统集成)扩展。

VK_KHR_surface。它暴露了一个 VkSurfaceKHR对象,它代表了一种抽象的表面类型,用于呈现渲染的图像。我们程序中的表面将由我们已经用GLFW打开的窗口来支持。实际上我们已经启用了它,因为它被包含在glfwGetRequiredInstanceExtensions返回的列表中。

Vulkan Tutorial 2 presentation_第1张图片

在调试回调的下面添加一个surface类成员。

VkSurfaceKHR surface;

VkWin32SurfaceCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
//hwnd和hinstance。这些是窗口和进程的句柄。
createInfo.hwnd = glfwGetWin32Window(window);
//glfwGetWin32Window函数用于从GLFW窗口对象获得原始HWND。
//GetModuleHandle调用返回当前进程的HINSTANCE手柄。
createInfo.hinstance = GetModuleHandle(nullptr);

//创建表面,其中包括实例参数、表面创建细节、自定义分配器和表面句柄要存储的变量。
if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) {
    throw std::runtime_error("failed to create window surface!");
}

GLFW没有提供销毁surface的特殊函数,但这可以很容易地通过原始API完成:确保在实例之前,surface被销毁了。

void cleanup() {
        ...
        vkDestroySurfaceKHR(instance, surface, nullptr);
        vkDestroyInstance(instance, nullptr);
        ...
    }

查询展示支持

尽管Vulkan的实现可能支持窗口系统集成,但这并不意味着系统中的每个设备都支持它。因此我们需要扩展isDeviceSuitable以确保设备可以向我们创建的表面呈现图像。由于呈现是一个队列特定的功能,问题实际上是要找到一个支持向我们创建的表面呈现的队列家族。

struct QueueFamilyIndices {
    std::optional graphicsFamily;
    std::optional presentFamily;

    bool isComplete() {
        return graphicsFamily.has_value() && presentFamily.has_value();
    }
};


VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);

//然后简单地检查布尔值,并存储展示家庭队列索引

if (presentSupport) {
    indices.presentFamily = i;
}

//创建展示队列并检索VkQueue句柄。为句柄添加一个成员变量
VkQueue presentQueue;

//要有多个VkDeviceQueueCreateInfo结构来创建两个家族的队列
QueueFamilyIndices indices = findQueueFamilies(physicalDevice);

std::vector queueCreateInfos;
std::set uniqueQueueFamilies = {indices.graphicsFamily.value(), indices.presentFamily.value()};

float queuePriority = 1.0f;
for (uint32_t queueFamily : uniqueQueueFamilies) {
    VkDeviceQueueCreateInfo queueCreateInfo{};
    queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
    queueCreateInfo.queueFamilyIndex = queueFamily;
    queueCreateInfo.queueCount = 1;
    queueCreateInfo.pQueuePriorities = &queuePriority;
    queueCreateInfos.push_back(queueCreateInfo);
}

//并修改VkDeviceCreateInfo以指向矢量

createInfo.queueCreateInfoCount = static_cast(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();

//如果队列家族是相同的,那么我们只需要传递一次它的索引
vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);

6 交换链

Vulkan没有 “默认帧缓冲区”的概念,因此它需要一个基础设施,在我们将缓冲区在屏幕上可视化之前,它将拥有我们要渲染的缓冲区。这个基础设施被称为交换链,必须在Vulkan中明确创建。

交换链本质上是一个等待被呈现到屏幕上的图像队列。我们的应用程序将获取这样的图像来绘制它,然后将其返回到队列中。队列究竟如何工作以及从队列中呈现图像的条件取决于交换链是如何设置的,但是交换链的一般目的是使图像的呈现与屏幕的刷新率同步。

Vulkan Tutorial 2 presentation_第2张图片

检查交换链的支持情况

由于各种原因,并不是所有的显卡都能够直接将图像呈现在屏幕上,例如因为它们是为服务器设计的,没有任何显示输出。其次,由于图像呈现与窗口系统和与窗口相关的表面有很大的关系,所以它实际上不是Vulkan核心的一部分。你必须在查询到VK_KHR_swapchain设备扩展的支持后启用它。

Vulkan头文件提供了一个很好的宏VK_KHR_SWAPCHAIN_EXTENSION_NAME,它被定义为VK_KHR_swapchain。使用这个宏的好处是,编译器会捕捉到错误的拼写。

const std::vector deviceExtensions = {
    VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
bool isDeviceSuitable(VkPhysicalDevice device) {
    QueueFamilyIndices indices = findQueueFamilies(device);

    bool extensionsSupported = checkDeviceExtensionSupport(device);

    return indices.isComplete() && extensionsSupported;
}

//修改函数的主体以列举扩展名,并检查所有需要的扩展名是否在其中
 bool checkDeviceExtensionSupport(VkPhysicalDevice device) {
        uint32_t extensionCount;
        vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr);

        std::vector availableExtensions(extensionCount);
        vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, availableExtensions.data());

        std::set requiredExtensions(deviceExtensions.begin(), deviceExtensions.end());

        for (const auto& extension : availableExtensions) {
            requiredExtensions.erase(extension.extensionName);
        }

        return requiredExtensions.empty();
    }

仅仅检查一个交换链是否可用是不够的,因为它可能实际上与我们的窗口表面不兼容。创建交换链还涉及到比创建实例和设备更多的设置,所以在我们能够继续之前,我们需要查询一些更多的细节。基本上有三种属性是我们需要检查的。

  • 基本表面能力(交换链中图像的最小/最大数量,图像的最小/最大宽度和高度)
  • 表面格式(像素格式、色彩空间)
  • 可用的表现模式

我们将使用一个结构来传递这些细节,一旦它们被查询到。上述三种类型的属性是以下列结构和结构列表的形式出现的:

struct SwapChainSupportDetails {
    VkSurfaceCapabilitiesKHR capabilities;
    std::vector formats;
    std::vector presentModes;
};

现在我们将创建一个新的函数querySwapChainSupport,它将填充这个结构。

SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) 
    {
        SwapChainSupportDetails details;
//表面能力开始。这些属性的查询很简单,并被返回到一个VkSurfaceCapabilitiesKHR结构中
        vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities);

//查询支持的表面格式。因为这是一个结构列表,它遵循熟悉的2个函数调用的仪式
        uint32_t formatCount;
        vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);

        if (formatCount != 0) {
            details.formats.resize(formatCount);
            vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data());
        }

//查询支持的演示模式
        uint32_t presentModeCount;
        vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr);

        if (presentModeCount != 0) {
            details.presentModes.resize(presentModeCount);
            vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data());
        }

        return details;
    }

如果满足了swapChainAdequate的条件,那么支持肯定是足够的,但是仍然可能有许多不同的模式,有不同的优化。我们现在要写几个函数来找到最佳交换链的正确设置。有三种类型的设置需要确定。

  • 表面格式(颜色深度)
  • 演示模式(将图像 “交换”到屏幕上的条件)
  • 交换范围(交换链中图像的分辨率)

对于这些设置中的每一个,我们都会有一个理想的值,如果有的话,我们就会采用这个值,否则,我们就会建立一些逻辑来寻找下一个最佳值。

VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) 
//每个VkSurfaceFormatKHR'条目包含一个format’和一个colorSpace'成员。
//format成员指定了颜色通道和类型。
    {
        for (const auto& availableFormat : availableFormats) {
            if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {//是否支持SRGB颜色空间
                return availableFormat;
            }
        }

        return availableFormats[0];
    }

//演示模式可以说是交换链最重要的设置,因为它代表了向屏幕显示图像的实际条件
VkPresentModeKHR chooseSwapPresentMode(const std::vector& 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) {
        if (capabilities.currentExtent.width != std::numeric_limits::max()) {
            return capabilities.currentExtent;
        }
        else {
            int width, height;
            glfwGetFramebufferSize(window, &width, &height);

            VkExtent2D actualExtent = {
                static_cast(width),
                static_cast(height)
            };
//在minImageExtent'和maxImageExtent’的范围内挑选与窗口最匹配的分辨率。
            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;
        }
    }

 GLFW在测量尺寸时使用两个单位:像素和屏幕坐标。例如,我们之前在创建窗口时指定的分辨率{WIDTH, HEIGHT}是以屏幕坐标测量的。但是Vulkan是用像素工作的,所以交换链的范围也必须用像素指定。

如果你使用的是高DPI显示器(比如苹果的Retina显示器),屏幕坐标并不对应于像素。相反,由于像素密度较高,窗口的像素分辨率会比屏幕坐标的分辨率大。因此,如果Vulkan不为我们固定交换范围,我们就不能只使用原来的{WIDTH, HEIGHT}。相反,我们必须使用glfwGetFramebufferSize来查询窗口的像素分辨率,然后再与最小和最大图像范围相匹配。

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 + 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;
//每个图像所包含的层数。除非你正在开发一个立体的3D应用程序,否则这总是1
        createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
//我们将在交换链中使用图像的哪种操作
}

接下来,我们需要指定如何处理将在多个队列家族中使用的交换链图像。在我们的应用程序中,如果图形队列系列与演示队列不同,就会出现这种情况。我们将从图形队列中绘制交换链中的图像,然后在演示队列中提交它们。有两种方法来处理从多个队列访问的图像。

  • vk_sharing_mode_exclusive。一个图像一次由一个队列家族拥有,在另一个队列家族中使用它之前,必须明确转移所有权。这个选项提供了最好的性能。
  • vk_sharing_mode_concurrent。图像可以在多个队列家族中使用,无需明确的所有权转移。
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;
        }

//我们可以指定在交换链中,如果支持某种变换(capabilities中的supportedTransforms
//要指定你不想要任何变换,只需指定当前的变换。
        createInfo.preTransform = swapChainSupport.capabilities.currentTransform;
//是否应该使用alpha通道与窗口系统中的其他窗口进行混合
        createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
        createInfo.presentMode = presentMode;
        createInfo.clipped = VK_TRUE;

        createInfo.oldSwapchain = VK_NULL_HANDLE;

它应该在设备前用vkDestroySwapchainKHR来清理。

检索互换链图像

现在已经创建了交换链,所以剩下的就是检索其中的VkImage的手柄了。在后面的章节中,我们将在渲染操作中引用这些手柄。添加一个类成员来存储句柄。

std::vector swapChainImages;
//这些图像是由交换链的实现创建的,一旦交换链被销毁,它们将被自动清理

//在createSwapChain'函数的末尾添加了检索句柄的代码
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data());
//我们只指定了交换链中图像的最低数量,所以实现允许创建一个有更多图像的交换链。
//先用vkGetSwapchainImagesKHR查询最终的图像数量,然后调整容器的大小,最后再调用它来检索手柄。

swapChainImageFormat = surfaceFormat.format;
        swapChainExtent = extent;
//将我们为交换链图像选择的格式和范围存储在成员变量中。

7 图像视图

为了在渲染管道中使用任何VkImage,包括交换链中的对象,我们必须创建一个VkImageView对象。图像视图实际上是对图像的一种观察。它描述了如何访问图像以及访问图像的哪一部分,例如,如果它应该被当作一个没有任何mipmapping层的2D纹理深度纹理。

//编写一个createImageViews函数,为交换链中的每个图像创建一个基本的图像视图
//这样我们就可以在以后将它们作为颜色目标。
std::vector swapChainImageViews;

//创建createImageViews函数,并在交换链创建后立即调用它。

void createImageViews() {
//调整列表的大小,以适应我们将要创建的所有图像视图。
swapChainImageViews.resize(swapChainImages.size());

//设置循环,在所有交换链图像上进行迭代。
for (size_t i = 0; i < swapChainImages.size(); i++) {
            VkImageViewCreateInfo createInfo{};
//创建图像视图的参数在VkImageViewCreateInfo结构中指定。前面的几个参数是直接的。
            createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
            createInfo.image = swapChainImages[i];
//viewType 和 format字段指定了图像数据的解释方式
            createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
            createInfo.format = swapChainImageFormat;
//components字段允许你对颜色通道进行旋转,我们将坚持使用默认的映射。
            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;
//ubresourceRange字段描述了图像的目的是什么,应该访问图像的哪一部分。
//我们的图像将被用作颜色目标,没有任何mipmapping级别或多个层次。
            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!");
            }
        }

}

与图像不同,图像视图是由我们明确创建的,所以我们需要添加一个类似的循环,在程序结束时再次销毁它们:

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

    ...
}

你可能感兴趣的:(vulkan,c++)