窗口表面
前言
Vulkan 作为平台无关的 API ,需要通过 WSI (窗口系统集成)扩展与窗口系统交互。
在本章中,我们讨论的第一个扩展是 VK_KHR_surface
。
它提供了一个 vk::SurfaceKHR
窗口表面对象,该对象表示一种抽象的表面类型,用于呈现渲染后的图像。
关于 WSI 和 Surface:Vulkan-Guide [WSI]
实际上我们已经启用了此扩展,因为它在 glfwGetRequiredInstanceExtensions
返回的列表中。
该列表还包括我们将在接下来的几章中使用的其他 WSI 扩展。
离屏渲染不需要窗口系统,也就不需要这些扩展。
创建窗口表面
1. 基础结构
窗口表面需要在实例创建之后立即创建,因为它会影响物理设备的选择。 由于它还涉及了呈现和渲染的内容,推迟到现在才说明是为了使教程结构更加清晰。
首先在调试回调下方添加一个 m_surface
类成员。现在成员变量列表应该是:
GLFWwindow* m_window{ nullptr };
vk::raii::Context m_context;
vk::raii::Instance m_instance{ nullptr };
vk::raii::DebugUtilsMessengerEXT m_debugMessenger{ nullptr };
vk::raii::SurfaceKHR m_surface{ nullptr };
vk::raii::PhysicalDevice m_physicalDevice{ nullptr };
vk::raii::Device m_device{ nullptr };
vk::raii::Queue m_graphicsQueue{ nullptr };
添加一个函数 createSurface
,在实例创建和 setupDebugMessenger
之后调用它。
void initVulkan() {
createInstance();
setupDebugMessenger();
createSurface();
selectPhysicalDevice();
createLogicalDevice();
}
void createSurface() {
}
2. 创建资源
虽然 vk::raii::SurfaceKHR
对象及其用法与平台无关,但其创建并非如此,因为它取决于窗口系统的详细信息。
好消息是,所需的平台特定扩展已在 glfwGetRequiredInstanceExtensions
返回的列表中,无需手动添加。
我们将直接使用 GLFW 的 glfwCreateWindowSurface
函数,它自动为我们处理平台的差异,但下面的代码还不能运行:
if (glfwCreateWindowSurface( m_instance, m_window, nullptr, &m_surface) != VK_SUCCESS) {
throw std::runtime_error("failed to create window surface!");
}
因为 glfwCreateWindowSurface
接受的是原始的C风格的Intsance
和Surface
,所以我们必须这么做:
void createSurface() {
VkSurfaceKHR cSurface;
if (glfwCreateWindowSurface( *m_instance, m_window, nullptr, &cSurface ) != VK_SUCCESS) {
throw std::runtime_error("failed to create window surface!");
}
m_surface = vk::raii::SurfaceKHR( m_instance, cSurface );
}
这里 *m_instance
返回内部 vk::Instance
的引用,然后可以隐式转换为 VkInstance
。
我们使用 C 接口的 VkSurfaceKHR
类型接受句柄,并将句柄交给 raii
封装类进行管理。
呈现队列
Vulkan 支持窗口系统,但设备可能不支持。
我们需要扩展 isDeviceSuitable
以确保设备可以将图像呈现到我们创建的表面。
由于呈现是特定队列的功能,此问题实际上是寻找一个支持呈现到表面的队列族。
1. 获取呈现队列句柄
支持绘制的队列族和支持呈现的队列族可能不重叠,因此需要修改 QueueFamilyIndices
结构体,添加新内容:
struct QueueFamilyIndices {
std::optional<uint32_t> graphicsFamily;
std::optional<uint32_t> presentFamily;
bool isComplete() const {
return graphicsFamily.has_value() && presentFamily.has_value();
}
};
下面修改 findQueueFamilies
函数,以查找具有呈现到窗口表面的能力的队列族。
将下面的代码放在与 vk::QueueFlagBits::eGraphic
相同的循环中
if(physicalDevice.getSurfaceSupportKHR(i, m_surface)){
indices.presentFamily = i;
}
注意,它们最终很可能是同一个队列族,但在整个程序中,我们可以将它们视为独立的个体使用。
尽管如此,您可以添加额外的逻辑,偏好那些具有同时支持绘制和呈现功能的队列族的物理设备,从而提高性能。
2. 创建呈现队列
剩下的最后一件事是修改逻辑设备创建过程,从而创建呈现队列。在 m_graphicsQueue
下方添加一个成员变量:
vk::raii::Queue m_presentQueue{ nullptr };
接下来,我们需要多个 CreateInfo
结构体,以便从两个队列族创建队列。
一种优雅的方法是创建一个所需队列族的集合。让我们修改 createLogicalDevice
函数
std::vector<vk::DeviceQueueCreateInfo> queueCreateInfos;
const auto [graphics, present] = findQueueFamilies( m_physicalDevice );
std::set<uint32_t> uniqueQueueFamilies = { graphics.value(), present.value() };
constexpr float queuePriority = 1.0f;
for (uint32_t queueFamily : uniqueQueueFamilies) {
vk::DeviceQueueCreateInfo queueCreateInfo;
queueCreateInfo.queueFamilyIndex = queueFamily;
queueCreateInfo.setQueuePriorities( queuePriority );
queueCreateInfos.emplace_back( queueCreateInfo );
}
这里使用了 C++17 的结构化绑定以快速提取类成员。
然后修改 setQueueCreateInfos
以指向创建信息数组:(参数变量名加个s
)
createInfo.setQueueCreateInfos( queueCreateInfos );
最后修改函数末尾获取队列句柄的语句:
m_graphicsQueue = m_device.getQueue( graphics.value(), 0 );
m_presentQueue = m_device.getQueue( present.value(), 0 );
如果队列族相同,这两个句柄很可能具有相同的值,但这依然可以正常运行。
测试
现在构建与运行程序,保证没有报错。