Vulkan学习总结
把一个大象装冰箱,主要分三步
相比OpenGL,Vulkan的驱动层更薄,更多的工作交给了应用层去做。但是总得来讲,完成一个Vulkan应用程序,大体上还是分为这三个步骤:
初始化 Vulkan。这一步主要是完成一些初始配置,资源加载等。类比OpenGL,OpenGL是状态机模式,我们可以把OpenGL想象成一个冷库,我们使用冷库的时候必须进入冷库中去。我们的每一行代码glXXXX的执行,其实就好比修改了冷库的温度,那么之后的所有操作都要在这个温度下执行了。而Vulkan可以理解成一个冰箱,我们把冰箱放在家里,我们的所有操作是针对这个冰箱而言,冰箱温度很低,但是不影响我们自身所处的环境。我们还可以有多台冰箱,不同的冰箱可以 设定不同的温度,互不影响。
类比一下初始化Vulkan,就是我们给Vulkan这台冰箱提供一个冰箱所需要的工作环境一样,然后把我们的模型数据像食材一样放进冰箱,再准备好插电板之类的东东,最后通电~
- 主循环。主循环就是不听的完成一帧一帧画面的渲染,并将画面呈现在window上。这背后是GPU的大量运算,也就是冰箱启动之后,氟利昂不停的将热量从冰箱内移动到冰箱外。
- 清理。当我们的冰箱不再使用之后,冰箱内的各种食材是要拿出来的,否则在里面都坏了....同样我们还要把之前申请的各类资源释放掉。
完成了以上三步,我们已经可以把大象装进冰箱了。不过实际上,还是有其他“一(亿)点点”工作要做......
初始化
对于应用开发者而言,主要关注的是接口使用方法,开发者无需了解驱动的实现逻辑以及硬件的逻辑。但是由于Vulkan接口比较薄,一些接口还是带有底层驱动或者硬件的影子,理解起来比较困难,因此后续还是以冰箱的例子类比,便于加深理解。
创建实例(Create Instance)
什么是Vulkan实例?官方教程的原话是: The instance is the connection between your application and the Vulkan library and creating it involves specifying some details about your application to the driver.
直接的意思就是instance是应用于Vulkan library的链接。如何理解呢?我们把我们的应用想象成一个房子,在房子里面我们可以干各种事情,房子中有一个专门的房间是用来储存食材的,这个房间就可以理解成是一个instance。
VkInstance instance;
VkApplicationInfo appInfo = {};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "LittleVulkanEngine App";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_0;
VkInstanceCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
auto extensions = getRequiredExtensions();
createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
createInfo.ppEnabledExtensionNames = extensions.data();
createInfo.enabledLayerCount = 0;
createInfo.pNext = nullptr;
if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
throw std::runtime_error("failed to create instance!");
}
其中大部分入参的含义都很明显,需要特别注意两个:
1,pNext,表示对当前入参结构体的一个扩展,一般是nullptr。
2,extensions,这个入参是一个vector,类型是const char *,即字符串的vector。
如果GPU是一个不支持图形渲染的GPU的话,那么返回Null,否则返回的vector中是会携带字符串: VK_KHR_surface 的。
另外,还需要注意的是,VkInstance的类型实际上是一个指针。因此在使用instance的函数,可以直接拷贝传值,无需担心效率问题以及拷贝了多个实例。
创建Surface(Create Surface)
官方文档对Surface的描述是这样的: It exposes a VkSurfaceKHR
object that represents an abstract type of surface to present rendered images to.
应用渲染好的图片,需要在Window上显示出来,但是不同操作系统的Window显示协议,逻辑都差异很大,Vulkan是感知不到操作系统的,因为Vulkan是夸平台的,那么这里的Surface其实就是一个对接window的载体。
VkSurfaceKHR surface;
if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) {
throw std::runtime_error("failed to craete window surface");
}
正如前面提到的,instance是一个指针,因此当做入参可以直接使用拷贝传值的方式。
其中的第三个函数,其类型为VkAllocationCallbacks ,一般传入nullptr即可。
选择物理设备(Pick Physical Device)
选择物理设备,很好理解,就是GPU。代码也很直观:
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
if (deviceCount == 0) {
throw std::runtime_error("failed to find GPUs with Vulkan support!");
}
std::cout << "Device count: " << deviceCount << std::endl;
std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());
for (const auto &device : devices) {
if (isDeviceSuitable(device)) {
physicalDevice = device;
break;
}
}
if (physicalDevice == VK_NULL_HANDLE) {
throw std::runtime_error("failed to find a suitable GPU!");
}
上面的代码是典型的Vulkan风,先枚举物理设备的数量,然后枚举出所有物理设备,最后调用isDeviceSuitable方法,选取到合适的物理设备。这里的isDeviceSuitable()我们可以给出最初始的版本,即我们想选取独立显卡,并且显卡支持几何着色器。与pick Physical Device类似,这部分代码都很Vulkan:
bool isDeviceSuitable(VkPhysicalDevice device) {
VkPhysicalDeviceProperties deviceProperties;
VkPhysicalDeviceFeatures deviceFeatures;
vkGetPhysicalDeviceProperties(device, &deviceProperties);
vkGetPhysicalDeviceFeatures(device, &deviceFeatures);
return deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU &&
deviceFeatures.geometryShader;
}
因为选取到物理设备之后,还要查看一下这个物理设备的Queue Family。对于Queue Family的理解,类比冰箱这个概念依然很合适,冰箱有冷藏,冷冻,软冻等等功能,同样GPU也是如此,有一些Queue仅支持计算,有一些Queue仅支持传输,我们需要找的queue需要支持图形计算以及图形显示。这个Queue Family就类似冰箱说明书,告诉我们从下往上数,第三个门打开是冷藏箱。
uint32_t queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);
std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data());
依旧很Vulkan的代码,这里queueFamilies容器中已经保存了该queue family所支持的所有功能。
如果queueFamilies中的某个成员的queueFlags与VK_QUEUE_GRAPHICS_BIT进行&运算结果为true,即表明该成员支持图形计算。我们仅需要得到其下标即可。
创建逻辑设备(Creating Logical Device)
对逻辑设备的创建,还是可以用冰箱的类比。现在我们通过queue family知道了冰箱的功能,第三个箱是冷冻箱,这就是我们想要的。然而使用冷冻箱,还是需要用隔板把冷冻箱隔离成几个格挡,我们放食材进冷冻箱的时候,需要指明使用的是哪一个格挡。这里的格挡,就是Queue,而整个冷冻箱,就是logical Device。
代码也如同这个类比一样,十分的显而易见:
VkDeviceQueueCreateInfo queueCreateInfo{};
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = indices.graphicsFamily.value();
queueCreateInfo.queueCount = 3;
float queuePriority[] = {1.0f, 0.0, 0.0};
queueCreateInfo.pQueuePriorities = &queuePriority;
这里的queueCreateInfo.queueFamilyIndex,就是我们在查找queueFamily中查出来的我们想要的QueueFamily的位置。也就是冷冻箱的位置。
queueCreateInfo.queueCount,就是我们要把这个冷冻箱,分成多少个格挡。queuePriority指明了不同格挡的使用优先级。
定义好了Queue的创建信息,就可以来创建Logical Device了:
VkPhysicalDeviceFeatures deviceFeatures{};
VkDeviceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
createInfo.pQueueCreateInfos = &queueCreateInfo;
createInfo.queueCreateInfoCount = 1;
createInfo.pEnabledFeatures = &deviceFeatures;
createInfo.enabledExtensionCount = 0;
if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) {
throw std::runtime_error("failed to create logical device!");
}
其中,deviceFeatures是选取物理设备时候获取到的,我们这里还不涉及具体要使用的特性,因此直接进行默认初始化。
创建玩逻辑设备之后,如果需要获取具体的queue,可以使用如下代码:
VkQueue graphicsQueue;
vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue);
回到Surface
之前说到,queueFamily中能够查到是否支持图形计算,一般而言支持图形计算的GPU也支持把图形显示到window中去,但是个别GPU这两个功能不是同时支持的,因此我们需要进一步检测queueFamily是否支持图形显示功能。
VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface_, &presentSupport);
这里需要创建的device同时包含两个queue,因此create Info需要做一些调整:
std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
std::set<uint32_t> uniqueQueueFamilies = {indices.graphicsFamily, indices.presentFamily};
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);
}
VkPhysicalDeviceFeatures deviceFeatures = {};
deviceFeatures.samplerAnisotropy = VK_TRUE;
VkDeviceCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();
createInfo.pEnabledFeatures = &deviceFeatures;
createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
同样,我们可以调用vkGetDeviceQueue拿到这个queue的指针,实际上,一般这两个变量中存的地址是同一个地址。
创建交换链(Create Swap Chain)
Swap Chain的概念,和window的缓存模式有关,当前一般使用的缓存模式是三缓存模式,即window上显示一张图片,有两张图片是处于缓存区的。这个缓存区,称之为framebuffer,缓存区缓存好的图片交替显示到window,控制这种交替逻辑的对象,称之为交换链。
同样,创建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());
std::set<std::string> requiredExtensions(deviceExtensions.begin(), deviceExtensions.end());
for (const auto& extension : availableExtensions) {
requiredExtensions.erase(extension.extensionName);
}
return requiredExtensions.empty();
}
依然是浓浓Vulkan风的代码,创建逻辑设备的代码需要调整一下create Info,加上如下两句:
createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
额外的,我们还要检测SwapChain是否与Window Surface兼容,需要检测三项:
struct SwapChainSupportDetails {
VkSurfaceCapabilitiesKHR capabilities;
std::vector<VkSurfaceFormatKHR> formats;
std::vector<VkPresentModeKHR> presentModes;
}details;
这三项的获取方式如下:
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities);
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());
}
其中,VkSurfaceFormatKHR 表示了Surface的颜色格式信息,我们选取非线性SRGB颜色。这代表了gamma校准是2.2的RGB颜色。
for (const auto &availableFormat : availableFormats) {
if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB &&
availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
return availableFormat;
}
}
VkPresentModeKHR一般应用都选择使用 VK_PRESENT_MODE_FIFO_KHR ,但是如果为了能够发挥GPU的全部性能, VK_PRESENT_MODE_MAILBOX_KHR 是可以的,还不会造成画面割裂。 VK_PRESENT_MODE_IMMEDIATE_KHR 虽然能够发挥出GPU全部性能,但是会造成画面割裂。
VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) {
return VK_PRESENT_MODE_FIFO_KHR;
}
VkExtent2D ,这一项设置的原因,是因为我们屏幕坐标和像素坐标不一致的问题。一般屏幕中,当我们设置了window的大小是800600,并不是代表了window宽800像素,而是window的宽是800 1/96英寸。一般屏幕的dpi设置为96,因此,我们看到的一个像素所占的单位正好是1/96英寸。但是实际上,有一些显示器,比如苹果的显示器,并不是这样的。因此需要一些处理。
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;
}
}
至此,所有创建SwapChain的信息已经具备,可以进行创建:
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;
minImageCount 制定了swapchain的缓存数,如果要用3缓存模式,这里可以设置为3.
imageArrayLayers 一般为1,除非做VR开发。
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT一般直接渲染使用,如果是延迟渲染,则可以修改为 VK_IMAGE_USAGE_TRANSFER_DST_BIT
,对比OpenGL,延迟渲染需要将渲染结果储存在一个纹理中。
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
}
上面的代码定义了presentQueue和GraphicsQueue不相同时,应当如何处理。
createInfo.preTransform = swapChainSupport.capabilities.currentTransform;
该参数定义了图像是否翻转,之类的变换。
createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;
createInfo.oldSwapchain = VK_NULL_HANDLE;
clipped参数定义了如果window被遮挡的话,相关的像素是否需要进行计算。oldSwapchain定义了一个之前的SwapChain,举个例子,如果window的大小变化了的话,这时候需要创建一个新的swapChain,创建新的Swapchain的时候,老的Swapchain需要当成入参传进来,否则有些操作系统下会创建失败。
ok,至此终于可以创建Swapchain了:
if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) {
throw std::runtime_error("failed to create swap chain!");
}
最后,与Queue类似,我们可以拿到Swapchain中的image:
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data());
创建图片视图(Create Image View)
Image View的作用,就是其字面意思。我们在渲染管线中使用任何对象,都需要创建一个Image View对象。
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;
}
components 成员定义了我们是否要把图片映射成一个单色图。subresourceRange定义当前图片如何使用,因为图片也可以当成一个很大的数组来使用。
最后是创建命令:
if (vkCreateImageView(device, &createInfo, nullptr, &swapChainImageViews[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to create image views!");
}
创建渲染通道(Create Render Pass)
图形渲染管线是模型光栅化的钩子函数的执行顺序。渲染管线的输入是vertex buffer,输出是frame buffer。vertex buffer当前还没创建,我们可以硬编码将定点数据写在vertex shader中。普通的渲染管线主要是由vertex shader和fragment shader构成,我们需要将这两个shader文件进行编译,编译成.spv格式,然后再将.spv格式的文件导入并编码。
可以再CmakeList.txt中写入如下脚本,这样每次重构cmake工程即可执行将shader编译成spv的命令
if (CMAKE_SYSTEM_NAME MATCHES "Windows")
execute_process(COMMAND cmd /C "compile.bat" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
endif ()
其中compile.bat代码如下:
glslc ./shaders/simple_shader.vert -o ./shaders/simple_shader.vert.spv
glslc ./shaders/simple_shader.frag -o ./shaders/simple_shader.frag.spv
std::vector<char> LvePipeline::readFile(const std::string& filepath) {
std::ifstream file{filepath, std::ios::ate | std::ios::binary};
if (!file.is_open()) {
throw std::runtime_error("failed to open file: " + filepath);
}
size_t fileSize = static_cast<size_t>(file.tellg());//返回写入位置
std::vector<char> buffer(fileSize);
file.seekg(0);
file.read(buffer.data(), fileSize);
file.close();
return buffer;
}
基于此,可以创建 VkShaderModule
VkShaderModuleCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();
createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data());
这里的code就是ReadFile方法返回的buffer。
实际上使用时候,还需要将shader进一步包装成Shader stage:
VkPipelineShaderStageCreateInfo vertShaderStageInfo{};
vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
vertShaderStageInfo.module = vertShaderModule;
vertShaderStageInfo.pName = "main";
VkPipelineShaderStageCreateInfo fragShaderStageInfo{};
fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
fragShaderStageInfo.module = fragShaderModule;
fragShaderStageInfo.pName = "main";
VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo};
Vulkan不支持任何默认配置,不像OpenGL,因此渲染管线的其他不可编程阶段的配置,也需要显示初始化:
VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
inputAssembly.primitiveRestartEnable = VK_FALSE;
//类似GL_TRIANGLES的配置
VkViewport viewport{};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = (float) swapChainExtent.width;
viewport.height = (float) swapChainExtent.height;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
//viewport的配置
VkRect2D scissor{};
scissor.offset = {0, 0};
scissor.extent = swapChainExtent;
VkPipelineViewportStateCreateInfo viewportState{};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor;
VkPipelineRasterizationStateCreateInfo rasterizer{};
rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizer.depthClampEnable = VK_FALSE;
rasterizer.rasterizerDiscardEnable = VK_FALSE;//如果配置成true,输出定点不会到fragmentshader
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
rasterizer.lineWidth = 1.0f;
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
rasterizer.depthBiasEnable = VK_FALSE;//某些情况下,shadow mapping会使用
rasterizer.depthBiasConstantFactor = 0.0f; // Optional
rasterizer.depthBiasClamp = 0.0f; // Optional
rasterizer.depthBiasSlopeFactor = 0.0f; // Optional
VkPipelineMultisampleStateCreateInfo multisampling{};
multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampling.sampleShadingEnable = VK_FALSE;
multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
multisampling.minSampleShading = 1.0f; // Optional
multisampling.pSampleMask = nullptr; // Optional
multisampling.alphaToCoverageEnable = VK_FALSE; // Optional
multisampling.alphaToOneEnable = VK_FALSE; // Optional
VkPipelineColorBlendAttachmentState colorBlendAttachment{};
colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
colorBlendAttachment.blendEnable = VK_FALSE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; // Optional
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; // Optional
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; // Optional
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; // Optional
大部分配置一眼就能看出是什么含义,与OpenGL的一些配置能够对应的上。特别需要注意的是,这里有一些配置是支持动态修改的,无需重建整个pipeLine
VkDynamicState dynamicStates[] = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_LINE_WIDTH
};
VkPipelineDynamicStateCreateInfo dynamicState{};
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicState.dynamicStateCount = 2;
dynamicState.pDynamicStates = dynamicStates;
有些时候,我们还会用到uniform,这里我们也要进行配置:
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 0; // Optional
pipelineLayoutInfo.pSetLayouts = nullptr; // Optional
pipelineLayoutInfo.pushConstantRangeCount = 0; // Optional
pipelineLayoutInfo.pPushConstantRanges = nullptr; // Optional
if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
throw std::runtime_error("failed to create pipeline layout!");
}
至此,可以进行RenderPass的创建:
VkAttachmentDescription colorAttachment{};
colorAttachment.format = swapChainImageFormat;//需要匹配swapchain
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;//反走样才需要设置
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;//是否清理之前的Attachment
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;//保存渲染好的数据
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;//模板测试相关
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;//我们不关心渲染前Attachment的样式
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;// Images to be presented in the swap chain
但是目前,我们还不知道VkAttachmentDescription是给哪个Attachment设定的。因此我们要再设定一个:
VkAttachmentReference colorAttachmentRef{};
colorAttachmentRef.attachment = 0;//VkAttachmentDescription队列中第0个attachment
colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
目前还不涉及多个subpass的设定,这里我们只设定一个subpass:
VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;
subpass.pDepthStencilAttachment = &depthAttachmentRef;
这里的colorAttachmentRef实际上是被当成了一个数组。
最后,完成对RenderPass的创建:
VkRenderPassCreateInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassInfo.attachmentCount = 1;
renderPassInfo.pAttachments = &colorAttachment;
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;
if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) {
throw std::runtime_error("failed to create render pass!");
}
[以下内容为自己研究的部分]这里的配置官方文档描述让人有些费解,其实是这样,我们可以在shader中输出多个attachment:
#version 450
layout (location = 0) out vec4 outColor;
layout (location = 1) out vec4 outColor2;
layout(push_constant) uniform Push {
mat2 transform;
vec2 offset;
vec3 color;
} push;
void main() {
outColor = vec4(push.color, 1.0);
outColor2 = vec4(1.0, 0.0, 1.0, 1.0);
如果我们想输出outColor2的像素颜色,那我们需要将colorAttachmentRef申明成一个数组:
std::vector<VkAttachmentReference> colorArray = {colorAttachmentRef, colorAttachmentRef1};
VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 2;
subpass.pColorAttachments = colorArray.data();
subpass.pDepthStencilAttachment = &depthAttachmentRef;
配置subpass时候,同样需要修改:
std::array<VkAttachmentDescription, 3> attachments = {colorAttachment, colorAttachment, depthAttachment};
VkRenderPassCreateInfo renderPassInfo = {};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
renderPassInfo.pAttachments = attachments.data();
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;
renderPassInfo.dependencyCount = 1;
renderPassInfo.pDependencies = &dependency;
这样,pipeline和renderPass就不会有冲突。同样framebuff也要进行相应修改:
std::array<VkImageView, 3> attachments = {swapChainImageViews[i], swapChainImageViews[0],
depthImageViews[i]};
VkExtent2D swapChainExtent = getSwapChainExtent();
VkFramebufferCreateInfo framebufferInfo = {};
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebufferInfo.renderPass = renderPass;
framebufferInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
framebufferInfo.pAttachments = attachments.data();
framebufferInfo.width = swapChainExtent.width;
framebufferInfo.height = swapChainExtent.height;
framebufferInfo.layers = 1;
由于创建imageView需要涉及到内存申请,这里不过多讨论,我们把location=1的颜色只在3缓存中的第一张图片显示。
额外的,fixFunction部分的blend配置也需要修改:
configInfo.colorBlendAttachmentVec.push_back(configInfo.colorBlendAttachment);
configInfo.colorBlendAttachmentVec.push_back(configInfo.colorBlendAttachment);
configInfo.colorBlendInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
configInfo.colorBlendInfo.logicOpEnable = VK_FALSE;
configInfo.colorBlendInfo.logicOp = VK_LOGIC_OP_COPY; // Optional
configInfo.colorBlendInfo.attachmentCount = 2;
configInfo.colorBlendInfo.pAttachments = configInfo.colorBlendAttachmentVec.data();
configInfo.colorBlendInfo.blendConstants[0] = 0.0f; // Optional
configInfo.colorBlendInfo.blendConstants[1] = 0.0f; // Optional
configInfo.colorBlendInfo.blendConstants[2] = 0.0f; // Optional
configInfo.colorBlendInfo.blendConstants[3] = 0.0f; // Optional
这样配置后,我们就会看到一个颜色不断闪烁的图形了。
创建渲染管线(Create Pipeline)
直接上代码:
VkGraphicsPipelineCreateInfo pipelineInfo{};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount = 2;
pipelineInfo.pStages = shaderStages;
pipelineInfo.pVertexInputState = &vertexInputInfo;
pipelineInfo.pInputAssemblyState = &inputAssembly;
pipelineInfo.pViewportState = &viewportState;
pipelineInfo.pRasterizationState = &rasterizer;
pipelineInfo.pMultisampleState = &multisampling;
pipelineInfo.pDepthStencilState = nullptr; // Optional
pipelineInfo.pColorBlendState = &colorBlending;
pipelineInfo.pDynamicState = nullptr; // Optional
pipelineInfo.layout = pipelineLayout;
pipelineInfo.renderPass = renderPass;
pipelineInfo.subpass = 0;
pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // Optional
pipelineInfo.basePipelineIndex = -1; // Optional
pipelineInfo.subpass = 0;这行代码表明了,一个pipeline只是对应了一个renderpass的subpass。这里到延迟渲染的时候再进行补充。
if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) {
throw std::runtime_error("failed to create graphics pipeline!");
}
创建帧缓存(Create FrameBuffer)
创建FrameBuffer的代码很直接,每个attach输出到哪个VKimage需要定义清楚即可:
VkImageView attachments[] = {
swapChainImageViews[i]
};
VkFramebufferCreateInfo framebufferInfo{};
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebufferInfo.renderPass = renderPass;
framebufferInfo.attachmentCount = 1;
framebufferInfo.pAttachments = attachments;
framebufferInfo.width = swapChainExtent.width;
framebufferInfo.height = swapChainExtent.height;
framebufferInfo.layers = 1;
特别需要注意的是,这里的framebuffer是和renderPass绑定的。而不是和subpass,这里猜测可能是和延迟渲染的优化有关,延迟渲染过程中无需像OpenGL一样需要先通过framebuffer渲染到一个纹理。否则定义subpass的意义是什么呢?需要进一步深入学习!
创建命令缓存区(Create CommandBuffer)
这里的CommandBuffer并不是GPU中的一个缓存区,而是储存在CPU中。因此还不是间接渲染,因而这样渲染的效率,后续还需要考虑优化。
Vulkan要求使用CommandBuffer Pool来进行CommandBuffer的创建。
QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice);
VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value();
poolInfo.flags = 0; // Optional
if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) {
throw std::runtime_error("failed to create command pool!");
}
因为我们要执行的是图形渲染命令,所以这里需要和graphics queue进行绑定。
flags可以进行配置: VK_COMMAND_POOL_CREATE_TRANSIENT_BIT ,表示commandbuffer频繁更新,commandbuffer pool可以进行一些优化。
VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT :允许CommandBuffer被局部更新。
之后就可以申请Commandbuffer的内存:
commandBuffers.resize(swapChainFramebuffers.size());
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = (uint32_t) commandBuffers.size();
if (vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()) != VK_SUCCESS) {
throw std::runtime_error("failed to allocate command buffers!");
}
allocInfo.level,定义CommandBuffer的嵌套关系,当前可以先不关注。
for (size_t i = 0; i < commandBuffers.size(); i++) {
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = 0; // Optional
beginInfo.pInheritanceInfo = nullptr; // Optional
if (vkBeginCommandBuffer(commandBuffers[i], &beginInfo) != VK_SUCCESS) {
throw std::runtime_error("failed to begin recording command buffer!");
}
VkRenderPassBeginInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = renderPass;
renderPassInfo.framebuffer = swapChainFramebuffers[i];
renderPassInfo.renderArea.offset = {0, 0};
renderPassInfo.renderArea.extent = swapChainExtent;
VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}};
renderPassInfo.clearValueCount = 1;
renderPassInfo.pClearValues = &clearColor;
vkCmdBeginRenderPass(commandBuffers[i], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
vkCmdDraw(commandBuffers[i], 3, 1, 0, 0);
if (vkEndCommandBuffer(commandBuffers[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to record command buffer!");
}
}
这其中,flags的定义与commandbuffer的生命周期有关,当前先不关注。至此,CommandBuffer配置完毕。可以进行最基本的图形绘制了。
主循环
同步
主循环中不断循环执行以下三个操作:
1,从swapchain中获取image;
2,图形渲染结果到image;
3,image交给swapchain去显示。
这三个操作是异步执行的。如果不进行同步,可能我们还没完成渲染,就已经执行了将image交给swapchain的操作了,因此需要进行同步。
同步的方式有两种:
1, fences:主要用于GPU-CPU同步
2, semaphores :主要用于GPU-GPU同步;
还有一个更简单的,Time line semaphores ,就是简单的时间同步。但是为了避免把程序写成研究茴香豆的茴有几种写法,这里先不做进一步深入了解。
这里显然应该选择semaphores 。
三个方法,定义两个semaphores :
VkSemaphore imageAvailableSemaphore;
VkSemaphore renderFinishedSemaphore;
VkSemaphoreCreateInfo semaphoreInfo{};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphore) != VK_SUCCESS ||
vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphore) != VK_SUCCESS) {
throw std::runtime_error("failed to create semaphores!");
}
从swapchain中获取image
uint32_t imageIndex;
vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex);
图形渲染结果到image
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
VkSemaphore waitSemaphores[] = {imageAvailableSemaphore};
VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};//之前阶段可以不用等待
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSemaphores;//定义等待的信号
submitInfo.pWaitDstStageMask = waitStages;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffers[imageIndex];
VkSemaphore signalSemaphores[] = {renderFinishedSemaphore};
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;//定义发射的信号
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) {
throw std::runtime_error("failed to submit draw command buffer!");
}
在renderPass中,虽然我们只定义了一个subpass,但是获取image这步操作默认是一个subpass,但是在RenderPass执行到 VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
阶段的时候,还是没有获取到image的,因此需要定义 VkSubpassDependency ,指明在VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
,两个subpass是相互独立的。
VkSubpassDependency dependency{};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;//默认subpass
dependency.dstSubpass = 0;//renderpass中的subpass
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;//默认subpass直到VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT前,不与其他subpass进行关联
dependency.srcAccessMask = 0;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;subpass直到VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT前,不与其他subpass进行关联
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;//关联之后写入image
renderPassInfo.dependencyCount = 1;
renderPassInfo.pDependencies = &dependency;
image交给swapchain去显示
类似的,我们需要设置信号signalSemaphores的回调即可:
VkPresentInfoKHR presentInfo{};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;
VkSwapchainKHR swapChains[] = {swapChain};
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapChains;
presentInfo.pImageIndices = &imageIndex;
presentInfo.pResults = nullptr; // Optional
vkQueuePresentKHR(presentQueue, &presentInfo);
至此,初步工作已经做完,我们已经可以显示一个硬编码的三角形!
但是其实还是有最后一点点问题,回想我们使用OpenGL开发一个Window,如果GPU侧计算量特别大。假设GPU侧负荷很大,需要10s才能渲染一帧,那么我们滚动滚轮,快速的滚动,必然会卡着不动。假设我们连续滚动了10次滚轮,窗口会卡着不动然后经过100秒慢慢放大10次吗?
不会!窗口会经过30秒放大3次!
最开始开发应用的时候我对此也不理解,其实OpenGL侧做了隐藏,但是Vulkan来说,这种功能需要我们自己添加。当前我们的应用,是会一直卡着,要卡100秒然后放大10次之后window才可以进行操作。
原因是我们仅仅在GPU-GPU之间进行了同步,而没有给GPU-CPU做同步。
这里就需要使用fence进行同步了。首先我们给3缓存里面的每一帧添加一个
semaphores ,这样不同缓存之间的同步就独立了,不会互相受影响。
std::vector<VkSemaphore> imageAvailableSemaphores;
std::vector<VkSemaphore> renderFinishedSemaphores;
VkSemaphoreCreateInfo semaphoreInfo{};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS ||
vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to create semaphores for a frame!");
}
其次我们申明 currentFrame ,用于对不同缓存Semaphores的索引。
vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
...
VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]};
...
VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[currentFrame]};
currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
同样,对每一帧缓存定义一个fence:
inFlightFences.resize(MAX_FRAMES_IN_FLIGHT);
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;//一定要添加,因为默认fence为unsignal
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS ||
vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS ||
vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to create synchronization objects for a frame!");
}
}
}
在主循环中,可以添加fench同步:
void drawFrame() {
vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);
vkResetFences(device, 1, &inFlightFences[currentFrame]);
...
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {
throw std::runtime_error("failed to submit draw command buffer!");
}
...
}
VK_TRUE表明需要所有fence都完成才可以执行回调,但是这里只有一个fence,也就无所谓了。
为了提高程序的鲁棒性,还需要进一步优化,因为有时候可能我们的swapchain重建了之类的,导致意料之外的事情发生,这时候可能index和currentFrame不相等了。那我们就要再添加一个 imagesInFlight.resize(swapChainImages.size(), VK_NULL_HANDLE) fence。
vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
// Check if a previous frame is using this image (i.e. there is its fence to wait on)
if (imagesInFlight[imageIndex] != VK_NULL_HANDLE) {
vkWaitForFences(device, 1, &imagesInFlight[imageIndex], VK_TRUE, UINT64_MAX);
}
// Mark the image as now being in use by this frame
imagesInFlight[imageIndex] = inFlightFences[currentFrame];
简单的说,上面代码的含义,就是获取到imageIndex,再进行一次同步检测。
vkResetFences(device, 1, &inFlightFences[currentFrame]);
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {
throw std::runtime_error("failed to submit draw command buffer!");
}
vkResetFences的位置再挪一下,这样同步检测的边界情况就不再有任何问题。
enjoy~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。