Vulkan学习总结

把一个大象装冰箱,主要分三步

graph TD

A[application begin] -->B(Part1:Initialize Vulkan)

    B -->|success| C(Part2:MainLoop)

    C -->|if window resized| B

    C -->|update model| C
    C -->|should close window| D(Part3:Clean Up)
    

相比OpenGL,Vulkan的驱动层更薄,更多的工作交给了应用层去做。但是总得来讲,完成一个Vulkan应用程序,大体上还是分为这三个步骤:

  1. 初始化 Vulkan。这一步主要是完成一些初始配置,资源加载等。类比OpenGL,OpenGL是状态机模式,我们可以把OpenGL想象成一个冷库,我们使用冷库的时候必须进入冷库中去。我们的每一行代码glXXXX的执行,其实就好比修改了冷库的温度,那么之后的所有操作都要在这个温度下执行了。而Vulkan可以理解成一个冰箱,我们把冰箱放在家里,我们的所有操作是针对这个冰箱而言,冰箱温度很低,但是不影响我们自身所处的环境。我们还可以有多台冰箱,不同的冰箱可以 设定不同的温度,互不影响。

    类比一下初始化Vulkan,就是我们给Vulkan这台冰箱提供一个冰箱所需要的工作环境一样,然后把我们的模型数据像食材一样放进冰箱,再准备好插电板之类的东东,最后通电~

  2. 主循环。主循环就是不听的完成一帧一帧画面的渲染,并将画面呈现在window上。这背后是GPU的大量运算,也就是冰箱启动之后,氟利昂不停的将热量从冰箱内移动到冰箱外。
  3. 清理。当我们的冰箱不再使用之后,冰箱内的各种食材是要拿出来的,否则在里面都坏了....同样我们还要把之前申请的各类资源释放掉。

完成了以上三步,我们已经可以把大象装进冰箱了。不过实际上,还是有其他“一(亿)点点”工作要做......

初始化

对于应用开发者而言,主要关注的是接口使用方法,开发者无需了解驱动的实现逻辑以及硬件的逻辑。但是由于Vulkan接口比较薄,一些接口还是带有底层驱动或者硬件的影子,理解起来比较困难,因此后续还是以冰箱的例子类比,便于加深理解。

graph TB
    subgraph Initialize Vulkan
    a(CreateInstance)-->b(CreateSurface)
    b-->c(PickPhysicalDevice)
    c-->d(CreateLogicalDevice)
    d-->e(CreateSwapChain)
    e-->f(CreateImageViews)
    f-->g(CreateRenderPass)
    g-->h(CreateDescriptorSetLayout)
    h-->i(CreateGraphicsPipeline)
    i-->j(CreateCommandPool)
    j-->k(CreateColorResource)
    k-->l(CreateDepthResources)
    l-->m(CreateFramebuffers)
    m-->n(CreateTextureImage)
    n-->o(CreateTextureImageView)
    o-->p(CreateTextureSampler)
    p-->q(CreatePushConstants)
    q-->r(CreateVertexBuffers)
    r-->s(CreateIndexBuffers)
    s-->t(CreateUniformBuffers)
    t-->u(CreateDescriptorPool)
    u-->v(CreateDescriptorSets)
    v-->w(CreateCommandBuffers)
    w-->x(CreateSyncObjects)
    end

创建实例(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~


DR_humblegod
1 声望0 粉丝

引用和评论

0 条评论