ImGui中Vulkan的窗口渲染

先看看ImGui中的Vulkan窗口结构体:

struct ImGui_ImplVulkanH_Window
{
    int                 Width;
    int                 Height;
    VkSwapchainKHR      Swapchain;
    VkSurfaceKHR        Surface;
    VkSurfaceFormatKHR  SurfaceFormat;
    VkPresentModeKHR    PresentMode;
    VkRenderPass        RenderPass;
    VkPipeline          Pipeline;               // The window pipeline may uses a different VkRenderPass than the one passed in ImGui_ImplVulkan_InitInfo
    bool                ClearEnable;
    VkClearValue        ClearValue;
    uint32_t            FrameIndex;             // Current frame being rendered to (0 <= FrameIndex < FrameInFlightCount)
    uint32_t            ImageCount;             // Number of simultaneous in-flight frames (returned by vkGetSwapchainImagesKHR, usually derived from min_image_count)
    uint32_t            SemaphoreIndex;         // Current set of swapchain wait semaphores we're using (needs to be distinct from per frame data)
    ImGui_ImplVulkanH_Frame*            Frames;
    ImGui_ImplVulkanH_FrameSemaphores*  FrameSemaphores;

    ImGui_ImplVulkanH_Window()
    {
        memset((void*)this, 0, sizeof(*this));
        PresentMode = (VkPresentModeKHR)~0;     // Ensure we get an error if user doesn't set this.
        ClearEnable = true;
    }
};

后文所做的事情无非就是把这个结构体填充完毕。

Setup Vulkan

首先先声明一下后面会使用的变量

static VkAllocationCallbacks* g_Allocator = NULL;
static VkInstance               g_Instance = VK_NULL_HANDLE;
static VkPhysicalDevice         g_PhysicalDevice = VK_NULL_HANDLE;
static VkDevice                 g_Device = VK_NULL_HANDLE;
static uint32_t                 g_QueueFamily = (uint32_t)-1;
static VkQueue                  g_Queue = VK_NULL_HANDLE;
static VkDebugReportCallbackEXT g_DebugReport = VK_NULL_HANDLE;
static VkPipelineCache          g_PipelineCache = VK_NULL_HANDLE;
static VkDescriptorPool         g_DescriptorPool = VK_NULL_HANDLE;

static ImGui_ImplVulkanH_Window g_MainWindowData;// 这是imgui用来控制vulkan渲染到窗口的
static int                      g_MinImageCount = 2;
static bool                     g_SwapChainRebuild = false;

然后按顺序执行下列步骤。

创建Vulkan Instance

官方教程说Vulkan Instance维护了每个Vulkan应用的状态,因此必须先进行vulkan instance的创建。此文章也说明了vulkan instance的初始化过程中会加载并初始化GPU驱动。

VkInstanceCreateInfo create_info = {};
create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
create_info.enabledExtensionCount = extensions_count;
create_info.ppEnabledExtensionNames = extensions;
err = vkCreateInstance(&create_info, g_Allocator, &g_Instance);

在退出应用时需要自行销毁此instance。

获取GPU设备

下列代码只是获取了GPU设备信息之后选择了一个GPU设备作为此引用的vulkan设备,并把设备信息储存起来,没有进行其他操作。

uint32_t gpu_count;
err = vkEnumeratePhysicalDevices(g_Instance, &gpu_count, NULL);
check_vk_result(err);
IM_ASSERT(gpu_count > 0);

VkPhysicalDevice* gpus = (VkPhysicalDevice*)malloc(sizeof(VkPhysicalDevice) * gpu_count);
err = vkEnumeratePhysicalDevices(g_Instance, &gpu_count, gpus);
check_vk_result(err);

// If a number >1 of GPUs got reported, find discrete GPU if present, or use first one available. This covers
// most common cases (multi-gpu/integrated+dedicated graphics). Handling more complicated setups (multiple
// dedicated GPUs) is out of scope of this sample.
// 这里优先选择独显,如无独显则选择获取的第一个GPU设备
int use_gpu = 0;
for (int i = 0; i < (int)gpu_count; i++)
{
    VkPhysicalDeviceProperties properties;
    vkGetPhysicalDeviceProperties(gpus[i], &properties);
    if (properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)
    {
        use_gpu = i;
        break;
    }
}

g_PhysicalDevice = gpus[use_gpu];
free(gpus);

选择队列族

[Vulkan 的几乎所有操作,从绘制到加载纹理都需要将操作 指令提交给一个队列,然后才能执行。Vulkan 有多种不同类型的队列,它们属于不同的队列族,每个队列族的队列只允许执行特定的一部分指令。 比如,可能存在只允许执行计算相关指令的队列族和只允许执行内存传输的队列族。](Vulkan从入门到精通31-队列族和逻辑设备 - 知乎 (zhihu.com))

uint32_t count;
vkGetPhysicalDeviceQueueFamilyProperties(g_PhysicalDevice, &count, NULL);
VkQueueFamilyProperties* queues = (VkQueueFamilyProperties*)malloc(sizeof(VkQueueFamilyProperties) * count);
vkGetPhysicalDeviceQueueFamilyProperties(g_PhysicalDevice, &count, queues);
for (uint32_t i = 0; i < count; i++)
    if (queues[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)
    {
        g_QueueFamily = i;
        break;
    }
free(queues);
IM_ASSERT(g_QueueFamily != (uint32_t)-1);

创建逻辑设备

创建逻辑设备时需要传入queue的参数,因此需要先创建queue info才能创建设备。在创建设备时也会创建queue。 逻辑设备肯定就区别于物理设备,之后需要vulkan进行交互的都是逻辑设备。

int device_extension_count = 1;
const char* device_extensions[] = { "VK_KHR_swapchain" };
const float queue_priority[] = { 1.0f };
VkDeviceQueueCreateInfo queue_info[1] = {};
queue_info[0].sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queue_info[0].queueFamilyIndex = g_QueueFamily;
queue_info[0].queueCount = 1;
queue_info[0].pQueuePriorities = queue_priority;
VkDeviceCreateInfo create_info = {};
create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
create_info.queueCreateInfoCount = sizeof(queue_info) / sizeof(queue_info[0]);
create_info.pQueueCreateInfos = queue_info;
create_info.enabledExtensionCount = device_extension_count;
create_info.ppEnabledExtensionNames = device_extensions;
err = vkCreateDevice(g_PhysicalDevice, &create_info, g_Allocator, &g_Device);
check_vk_result(err);
vkGetDeviceQueue(g_Device, g_QueueFamily, 0, &g_Queue);

同样地,在程序退出时需要调用kDestroyDevice。

创建描述符池

在vulkan中,descriptor大概是着色器使用的变量,最终组成discriptor set之后才能被shader使用。这篇文章对descriptor解释得蛮清楚。

v2-61ccfca7561dc80ff9e2cebfeaa446e3_1440w

VkDescriptorPoolSize pool_sizes[] =
{
    { VK_DESCRIPTOR_TYPE_SAMPLER, 1000 },
    { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 },
    { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000 },
    { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1000 },
    { VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1000 },
    { VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1000 },
    { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1000 },
    { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1000 },
    { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1000 },
    { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1000 },
    { VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1000 }
};
VkDescriptorPoolCreateInfo pool_info = {};
pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
pool_info.maxSets = 1000 * IM_ARRAYSIZE(pool_sizes);
pool_info.poolSizeCount = (uint32_t)IM_ARRAYSIZE(pool_sizes);
pool_info.pPoolSizes = pool_sizes;
err = vkCreateDescriptorPool(g_Device, &pool_info, g_Allocator, &g_DescriptorPool);
check_vk_result(err);

创建SwapChain

首先要使用某些方法创建窗口。

以GLFW为例,可以先用其创建一个窗口。

if (!glfwInit())
    return 1;

glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
GLFWwindow* window = glfwCreateWindow(1920, 1080, "Doxel", NULL, NULL);

接下来就是vulkan时间。

预先准备

创建surface

vulkan中有WSI(Window Surface Intergration)的概念,也就是将vulkan渲染的结果显示在某个window上。

surface的创建方法如下:

VkSurfaceKHR surface;
glfwCreateWindowSurface(g_Instance, window, g_Allocator, &surface);

最终会进行如下过程。

vkGetInstanceProcAddr(instance, "vkCreateWin32SurfaceKHR");这个函数似乎是用来获取指定instance中的vkCreateWin32SurfaceKHR函数地址,主要是为了节省时间优化效率,没什么功能上的意义。[参考]

在创建的时候获取了GLFW之前创建的窗口的Windows的handle,而后此handle会记录在VkWin32SurfaceCreateInfoKHR结构体内,然后使用vkCreateWin32SurfaceKHR创建Surface。

创建色彩空间和图像(像素)格式

而后创建色彩空间和格式信息。下面的imgui函数封装了访问和匹配vulkan设备支持的格式信息,返回的是VkSurfaceFormatKHR类型值,此类型由色彩空间和格式组成。

const VkFormat requestSurfaceImageFormat[] = { VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8_UNORM, VK_FORMAT_R8G8B8_UNORM };
const VkColorSpaceKHR requestSurfaceColorSpace = VK_COLORSPACE_SRGB_NONLINEAR_KHR;
wd->SurfaceFormat = ImGui_ImplVulkanH_SelectSurfaceFormat(g_PhysicalDevice, wd->Surface, requestSurfaceImageFormat, (size_t)IM_ARRAYSIZE(requestSurfaceImageFormat), requestSurfaceColorSpace);

创建呈现模式信息

呈现模式主要控制了,在渲染出一个图像后,采用什么策略显示到屏幕。各个策略如下,fifo策略在帧数拉满时没有画面撕裂。

  • VK_PRESENT_MODE_IMMEDIATE_KHR:你的程序提交的图像会立即传输到屏幕,这可能会导致撕裂;

  • VK_PRESENT_MODE_FIFO_KHR:交换链是个队列,显示的时候从队列头拿一个图像,程序插入渲染的图像到队列尾。如果队列满了程序就要等待,这差不多像是垂直同步,显示刷新的时刻就是垂直空白;

  • VK_PRESENT_MODE_FIFO_RELAXED_KHR:在最后一个垂直空白的时候,如果应用迟到,且队列为空,该模式才会和前面的那个有所不同。这样就不等到下一个垂直空白,图像会直接传输到屏幕,可能导致撕裂;

  • VK_PRESENT_MODE_MAILBOX_KHR:这是第二个模式的又一个变种,当队列满的时候,它不会阻塞应用,已经在队列中的图像会被新的替换。这个模式可以实现三重缓冲,避免撕裂,比使用双重缓冲的垂直同步减少很多延迟。

很显然,下列代码中的imgui函数也是封装了访问和匹配vulkan设备显示方法。

#ifdef IMGUI_UNLIMITED_FRAME_RATE
    VkPresentModeKHR present_modes[] = { VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_FIFO_KHR };
#else
    VkPresentModeKHR present_modes[] = { VK_PRESENT_MODE_FIFO_KHR };
#endif
    wd->PresentMode = ImGui_ImplVulkanH_SelectPresentMode(g_PhysicalDevice, wd->Surface, &present_modes[0], IM_ARRAYSIZE(present_modes));

创建SwapChain

首先初始化CreateInfo结构体。而后进行SwapChain的创建以及对应Image缓冲的创建。

VkSwapchainCreateInfoKHR info = {};
info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
info.surface = wd->Surface;
info.minImageCount = min_image_count;
info.imageFormat = wd->SurfaceFormat.format;
info.imageColorSpace = wd->SurfaceFormat.colorSpace;
info.imageArrayLayers = 1;
info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;           // Assume that graphics family == present family
info.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
info.presentMode = wd->PresentMode;
info.clipped = VK_TRUE;
info.oldSwapchain = old_swapchain;
VkSurfaceCapabilitiesKHR cap; // 这个capability包含了swapchain的最小缓存数和最大缓冲数之类
err = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_device, wd->Surface, &cap);

err = vkCreateSwapchainKHR(device, &info, allocator, &wd->Swapchain);
err = vkGetSwapchainImagesKHR(device, wd->Swapchain, &wd->ImageCount, nullptr);

在imgui里封装了FrameBuffer及其对应的Image和ImageView及需要属性,称作Frame,用来控制buffer和同步的。

struct ImGui_ImplVulkanH_Frame
{
    VkCommandPool       CommandPool;
    VkCommandBuffer     CommandBuffer;
    VkFence             Fence;
    VkImage             Backbuffer;
    VkImageView         BackbufferView;
    VkFramebuffer       Framebuffer;
};

struct ImGui_ImplVulkanH_FrameSemaphores
{
    VkSemaphore         ImageAcquiredSemaphore;
    VkSemaphore         RenderCompleteSemaphore;
};

创建Render Pass

vulkan中的Render Pass和Unity中Shader的Render Pass感觉上来说不同,拿延迟渲染举例,延迟渲染有渲染GBuffer的阶段和屏幕空间计算光照的部分(我也不知道为什么这么分成两个阶段),整个延迟渲染是一个Render Pass,但分为了GBuffer和屏幕空间两个pipeline,所以Render Pass应该是最高层的渲染流程。[引用]

// attachment
VkAttachmentDescription attachment = {};
attachment.format = wd->SurfaceFormat.format;
attachment.samples = VK_SAMPLE_COUNT_1_BIT;
attachment.loadOp = wd->ClearEnable ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
// attachment reference
VkAttachmentReference color_attachment = {};
color_attachment.attachment = 0;
color_attachment.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
// 一个Render Pass由若干Subpass组成
VkSubpassDescription subpass = {};
// bind point有compute和graphics两种,compute是计算管线,graphics是图形渲染管线
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &color_attachment;
// subpass依赖
VkSubpassDependency dependency = {};
// VK_SUBPASS_EXTERNAL表示了该subpass之前的subpass
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
// 0表示当前subpass吧大概
dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.srcAccessMask = 0;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
// Render Pass Create Info
VkRenderPassCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
info.attachmentCount = 1;
info.pAttachments = &attachment;
info.subpassCount = 1;
info.pSubpasses = &subpass;
info.dependencyCount = 1;
info.pDependencies = &dependency;
err = vkCreateRenderPass(device, &info, allocator, &wd->RenderPass);
check_vk_result(err);

创建ImageView

管线着色器不能直接访问图形对象。作为替代,image view相当于一个代理,代表了image所占据的连续内存区域,并且包含一些额外的成员用来对image进行读写。

An image view is quite literally a view into an image. It describes how to access the image and which part of the image to access, for example if it should be treated as a 2D texture depth texture without any mipmapping levels.

也就是说,ImageView帮你实现了访问Image像素之类的方法。

下文为每个Image都给创建一个ImageView。

因为swap chain里可以有多个image ,所以我们先发制人:为每一个image 创建一个imageView和framebuffer ,然后在绘画阶段选择一个正确的来使用。

VkImageViewCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
info.viewType = VK_IMAGE_VIEW_TYPE_2D;
info.format = wd->SurfaceFormat.format;
info.components.r = VK_COMPONENT_SWIZZLE_R;
info.components.g = VK_COMPONENT_SWIZZLE_G;
info.components.b = VK_COMPONENT_SWIZZLE_B;
info.components.a = VK_COMPONENT_SWIZZLE_A;
VkImageSubresourceRange image_range = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
info.subresourceRange = image_range;
for (uint32_t i = 0; i < wd->ImageCount; i++)
{
    ImGui_ImplVulkanH_Frame* fd = &wd->Frames[i];
    info.image = fd->Backbuffer;
    err = vkCreateImageView(device, &info, allocator, &fd->BackbufferView);
    check_vk_result(err);
}

创建FrameBuffer

Frame buffer(帧缓冲区)封装了 color buffer image和depth buffer image。其中color buffer image为从swap chain获取的image,frame buffer的创建个数需要跟swap chain的image的数量对应,比如,双缓冲的swap chain需要对应建立2个frame buffer。

Frame buffer主要负责将render pass跟attachment(ImageView)关联起来。[引用]v2-e3953472599247115e1d7635f8561786_720w

VkImageView attachment[1];
VkFramebufferCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
info.renderPass = wd->RenderPass;
info.attachmentCount = 1;
info.pAttachments = attachment;
info.width = wd->Width;
info.height = wd->Height;
info.layers = 1;
// 每个ImageView需要一个FrameBuffer
for (uint32_t i = 0; i < wd->ImageCount; i++)
{
    ImGui_ImplVulkanH_Frame* fd = &wd->Frames[i];
    attachment[0] = fd->BackbufferView;
    err = vkCreateFramebuffer(device, &info, allocator, &fd->Framebuffer);
    check_vk_result(err);
}

创建Command Pool和CommandBuffers及其他

Command Buffer是储存GPU绘制命令的Buffer。同时还创建每个Command Buffer的信号量,用来完成每个渲染批次之间的同步。和所有的Pool一样,Command Buffer的内存需要用Pool来分配。

可以看到Fence和Semaphore都会在vkQueueSubmit时作为参数传入,不同之处是,Fence用于阻塞CPU直到Queue中的命令执行结束(GPU、CPU之间的同步),而Semaphore用于不同的命令提交之间的同步(GPU、GPU之间的同步)。[引用]

VkResult err;
// 每个Image都要一个Command Buffer,不知道为啥
for (uint32_t i = 0; i < wd->ImageCount; i++)
{
    ImGui_ImplVulkanH_Frame* fd = &wd->Frames[i];
    ImGui_ImplVulkanH_FrameSemaphores* fsd = &wd->FrameSemaphores[i];
    {
        // 创建Command Pool
        VkCommandPoolCreateInfo info = {};
        info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
		// Command Buffer对象之间相互独立,不会被一起重置
        info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
        info.queueFamilyIndex = queue_family;
        err = vkCreateCommandPool(device, &info, allocator, &fd->CommandPool);
        check_vk_result(err);
    }
    {
        // 创建Command Buffer
        VkCommandBufferAllocateInfo info = {};
        info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
        info.commandPool = fd->CommandPool;
        // Primary Command Buffer不能被其他Command Buffer调用
        info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
        info.commandBufferCount = 1;
        err = vkAllocateCommandBuffers(device, &info, &fd->CommandBuffer);
        check_vk_result(err);
    }
    {
        // 创建fence
        VkFenceCreateInfo info = {};
        info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
        info.flags = VK_FENCE_CREATE_SIGNALED_BIT;
        err = vkCreateFence(device, &info, allocator, &fd->Fence);
        check_vk_result(err);
    }
    {
        // 创建获取和完成的信号量
        // 其中Aquired是为了等待上一帧渲染完之后,立即获取到下一个Image
        VkSemaphoreCreateInfo info = {};
        info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
        err = vkCreateSemaphore(device, &info, allocator, &fsd->ImageAcquiredSemaphore);
        check_vk_result(err);
        err = vkCreateSemaphore(device, &info, allocator, &fsd->RenderCompleteSemaphore);
        check_vk_result(err);
    }
}

创建管线

一个Sampler包含了LOD(mip),Filter(插值),adressMode(平铺方式(即uv范围之外如何进行采样))

VkSamplerCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
info.magFilter = VK_FILTER_LINEAR;
info.minFilter = VK_FILTER_LINEAR;
info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
info.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
info.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
info.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
info.minLod = -1000;
info.maxLod = 1000;
info.maxAnisotropy = 1.0f;
err = vkCreateSampler(v->Device, &info, v->Allocator, &bd->FontSampler);

Set DescriptorSetLayout

前文说到,一个descriptor大概算是Shader里的一个变量,大概需要将descriptor绑定到真实的GPU变量上。在以此创建LayoutBingding之后再创建Layout。

VkSampler sampler[1] = {bd->FontSampler};
VkDescriptorSetLayoutBinding binding[1] = {};
binding[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
binding[0].descriptorCount = 1;
binding[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
binding[0].pImmutableSamplers = sampler; 
VkDescriptorSetLayoutCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
info.bindingCount = 1;
info.pBindings = binding;
err = vkCreateDescriptorSetLayout(v->Device, &info, v->Allocator, &bd->DescriptorSetLayout);
check_vk_result(err);

如果要创建例如MVP之类的变量(错误的,变换矩阵push constant就好),LayoutBinding可以使用下列代码:

VkDescriptorSetLayoutBinding layout_binding = {};
// bingding表示是DescriptorSet中的第几个
layout_binding.binding = 0;
layout_binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
// count表示该DescriptorSet里有多少个Descriptor
layout_binding.descriptorCount = 1;
layout_binding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
layout_binding.pImmutableSamplers = NULL;

VkDescriptorSetLayoutCreateInfo descriptor_layout = {};
descriptor_layout.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
descriptor_layout.pNext = NULL;
descriptor_layout.bindingCount = 1;
descriptor_layout.pBindings = layout_bindings;

info.desc_layout.resize(NUM_DESCRIPTOR_SETS);
res = vkCreateDescriptorSetLayout(info.device, &descriptor_layout, NULL, info.desc_layout.data());
assert(res == VK_SUCCESS);

Set PipelineLayout

实际上在创建SetLayout之后应该创建Set实例的,但是imgui是先把这些layout信息创建好之后调用ImGui_ImplVulkan_CreatePipeline一起创建pipeline。

下列代码是将两个vec2类型的uniform绑定到了PipelineLayout里。

// Constants: we are using 'vec2 offset' and 'vec2 scale' instead of a full 3d projection matrix
// 和OpenGL一样的,需要手动指定变量的偏移和大小
VkPushConstantRange push_constants[1] = {};
push_constants[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
// 由于是第一个变量 因此offset == 0
push_constants[0].offset = sizeof(float) * 0;
push_constants[0].size = sizeof(float) * 4;
VkDescriptorSetLayout set_layout[1] = { bd->DescriptorSetLayout };
VkPipelineLayoutCreateInfo layout_info = {};
layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
layout_info.setLayoutCount = 1;
// 绑定数个DescriptorSetLayout
layout_info.pSetLayouts = set_layout;
layout_info.pushConstantRangeCount = 1;
layout_info.pPushConstantRanges = push_constants;
err = vkCreatePipelineLayout(v->Device, &layout_info, v->Allocator, &bd->PipelineLayout);
check_vk_result(err);

创建Shader

Shader Module

创建Vertex Shader和Fragment Shader。

VkShaderModuleCreateInfo vert_info = {};
vert_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
vert_info.codeSize = sizeof(__glsl_shader_vert_spv);
vert_info.pCode = (uint32_t*)__glsl_shader_vert_spv;
VkResult err = vkCreateShaderModule(device, &vert_info, allocator, &bd->ShaderModuleVert);
check_vk_result(err);

VkShaderModuleCreateInfo frag_info = {};
frag_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
frag_info.codeSize = sizeof(__glsl_shader_frag_spv);
frag_info.pCode = (uint32_t*)__glsl_shader_frag_spv;
VkResult err = vkCreateShaderModule(device, &frag_info, allocator, &bd->ShaderModuleFrag);
check_vk_result(err);

上面的codeSize居然是编译过后的spv中间代码的长度——也就是说__glsl_shader_frag_spv是纯纯spv中间码,何其的hardcode!

下面是编译之前的Vertex和Fragment Sahder。

#version 450 core
layout(location = 0) in vec2 aPos;
layout(location = 1) in vec2 aUV;
layout(location = 2) in vec4 aColor;
layout(push_constant) uniform uPushConstant { vec2 uScale; vec2 uTranslate; } pc;

out gl_PerVertex { vec4 gl_Position; };
layout(location = 0) out struct { vec4 Color; vec2 UV; } Out;

void main()
{
    Out.Color = aColor;
    Out.UV = aUV;
    gl_Position = vec4(aPos * pc.uScale + pc.uTranslate, 0, 1);
}
#version 450 core
layout(location = 0) out vec4 fColor;
layout(set=0, binding=0) uniform sampler2D sTexture;
layout(location = 0) in struct { vec4 Color; vec2 UV; } In;
void main()
{
    fColor = In.Color * texture(sTexture, In.UV.st);
}

Shader Stage

每个Stage对应一个可编程(大概)的着色器。看了下Stage类型蛮多的,真正能用与渲染的大概只有tessellation和geometry,compute大概可以辅助计算?

VkPipelineShaderStageCreateInfo stage[2] = {};
stage[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stage[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
stage[0].module = bd->ShaderModuleVert;
stage[0].pName = "main";

stage[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stage[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
stage[1].module = bd->ShaderModuleFrag;
stage[1].pName = "main";

创建Vertex Input Description

必须注意,description不是descriptor!!!!description只是用来描述Vertex Input的结构。Vertex Buffer在运行时进行绑定,下面的binding就是指绑定的buffer

Binding Point对Vertex Buffer的数据组织是无知的,它只关心数据块的大小和以什么样的速度(每Vertex/每Instance)更换对应的数据块,这些东西由Binding Description来解释。[引用]

Vertex Shader 对Vertex Buffer Binding是无知的,它所关心的只有Location。而Attribute Description就负责将一个Stride大小的数据块解释为Vertex Shader所关心的Location。

在后面记录command,进行管线与Buffer的绑定时需要按照此时的Description来。

// Vertex Input Binding Description
// 用来描述Vertex Input的大小和输入速度
VkVertexInputBindingDescription binding_desc[1] = {};
// 下面ImDrawVert其实就是包含了pos、uv、color三个属性的结构体。
binding_desc[0].stride = sizeof(ImDrawVert);
binding_desc[0].inputRate = VK_VERTEX_INPUT_RATE_VERTEX;

// Vertex Input Attribute Description
// 用来描述所有Vertex Input的格式(我觉得这里用layout来描述比较合适)
// 第一个属性是vector2的position,location为0,offset为0
VkVertexInputAttributeDescription attribute_desc[3] = {};
attribute_desc[0].location = 0;
attribute_desc[0].binding = binding_desc[0].binding;
attribute_desc[0].format = VK_FORMAT_R32G32_SFLOAT;
attribute_desc[0].offset = IM_OFFSETOF(ImDrawVert, pos);
// 第二个属性是vector2的uv,location为1,offset为8
attribute_desc[1].location = 1;
attribute_desc[1].binding = binding_desc[0].binding;
attribute_desc[1].format = VK_FORMAT_R32G32_SFLOAT;
attribute_desc[1].offset = IM_OFFSETOF(ImDrawVert, uv);
// 第三个属性为color的col,location为2,offset为16
attribute_desc[2].location = 2;
attribute_desc[2].binding = binding_desc[0].binding;
attribute_desc[2].format = VK_FORMAT_R8G8B8A8_UNORM;
attribute_desc[2].offset = IM_OFFSETOF(ImDrawVert, col);

v2-97dd2ffbd0fff80f1f8dfc734f6ea073_r

创建管线信息

创建一大堆CreateInfo为创建pipeline做准备。

Shader Stage

在这里绑定之前创建好的shader stage。

VkPipelineShaderStageCreateInfo stage[2] = {};
stage[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stage[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
stage[0].module = bd->ShaderModuleVert;
stage[0].pName = "main";
stage[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stage[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
stage[1].module = bd->ShaderModuleFrag;
stage[1].pName = "main";

Vertex Input

VkPipelineVertexInputStateCreateInfo vertex_info = {};
vertex_info.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertex_info.vertexBindingDescriptionCount = 1;
vertex_info.pVertexBindingDescriptions = binding_desc;
vertex_info.vertexAttributeDescriptionCount = 3;
// 用到了上文的Vertex Input Description
vertex_info.pVertexAttributeDescriptions = attribute_desc;

需要在录制Pipeline Command的阶段进行绑定。

Assembly

可以说就是index的信息。TRIANGLE_LIST就是每个三角形都有三个单独的顶点,蛮浪费空间的。

VkPipelineInputAssemblyStateCreateInfo ia_info = {};
ia_info.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
ia_info.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;

需要在录制Pipeline Command的阶段进行绑定。

Viewport

视窗描述了输出将被渲染到的帧缓冲区域。

VkPipelineViewportStateCreateInfo viewport_info = {};
viewport_info.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewport_info.viewportCount = 1;
// scissor是对视口进行裁剪
viewport_info.scissorCount = 1;

需要在录制Pipeline Command的阶段进行绑定。scissor也需要进行设置。

rasterization

光栅化是将三角形填充成片元的过程。

这里的polygon mode是指从三角面生成片元的方式。让人没想到的是,原来vk的光栅化的polygon mode不只是只有三角形填充,还有以下模式:

  • VK_POLYGON_MODE_FILL。填充

  • VK_POLYGON_MODE_LINE。只光栅化三角形的边,即wireframe。此模式可以设置linewidth,而最大linewidth取决于硬件,由此可见这是一个硬件处理的渲染方式(废话)。

  • VK_POLYGON_MODE_POINT。渲染顶点。

  • VK_POLYGON_MODE_FILL_RECTANGLE_NV specifies that polygons are rendered using polygon rasterization rules, modified to consider a sample within the primitive if the sample location is inside the axis-aligned bounding box of the triangle after projection. Note that the barycentric weights used in attribute interpolation can extend outside the range [0,1] when these primitives are shaded. Special treatment is given to a sample position on the boundary edge of the bounding box. In such a case, if two rectangles lie on either side of a common edge (with identical endpoints) on which a sample position lies, then exactly one of the triangles must produce a fragment that covers that sample during rasterization.

    Polygons rendered in VK_POLYGON_MODE_FILL_RECTANGLE_NV mode may be clipped by the frustum or by user clip planes. If clipping is applied, the triangle is culled rather than clipped.

    Area calculation and facingness are determined for VK_POLYGON_MODE_FILL_RECTANGLE_NV mode using the triangle’s vertices. 好像是说此模式只会将AABB包围盒里的采样点光栅化为片元(难道说普通的fill会把包围盒外边的也渲染?) 看了以上文字才发现自己图形学知识浅薄,之前一直知道四边形使用双线性双三次插值,以为三角形也可以类推,没想到三角形插值使用的是三角形的重心坐标系进行插值,不过原理也极其简单,把顶点A当作起点,AB,AC作为基向量就可以表示三角形内外所有位置的坐标,进而进行插值了。[文章]

VkPipelineRasterizationStateCreateInfo raster_info = {};
raster_info.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
raster_info.polygonMode = VK_POLYGON_MODE_FILL;
// 可以进行正面或背面剔除(由此可见 剔除是在rasterization进行的,shaderlab只是进行了一些配置)
raster_info.cullMode = VK_CULL_MODE_NONE;
// frontFace表示 识别为正面的三角形的点的顺序 这里表示当组成三角面的三个点顺序是顺时针时为正面
raster_info.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
raster_info.lineWidth = 1.0f;

Multisample

MSAA

VkPipelineMultisampleStateCreateInfo ms_info = {};
ms_info.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
ms_info.rasterizationSamples = (MSAASamples != 0) ? MSAASamples : VK_SAMPLE_COUNT_1_BIT;

ColorBlendAttachment

在fragment shader渲染完之后的像素如何和帧缓冲内的像素进行混合。

VkPipelineColorBlendAttachmentState color_attachment[1] = {};
color_attachment[0].blendEnable = VK_TRUE;
color_attachment[0].srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
color_attachment[0].dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
color_attachment[0].colorBlendOp = VK_BLEND_OP_ADD;
color_attachment[0].srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
color_attachment[0].dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
color_attachment[0].alphaBlendOp = VK_BLEND_OP_ADD;
// writemask指明要输出哪些通道
color_attachment[0].colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;

Stencil

VkPipelineDepthStencilStateCreateInfo depth_info = {};
depth_info.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;

ColorBlend

把上面的ColorBlendAttachment加进去。

VkPipelineColorBlendStateCreateInfo blend_info = {};
blend_info.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
blend_info.attachmentCount = 1;
blend_info.pAttachments = color_attachment;

Dynamic

有些属性是可以不修改管线,仅仅改变参数就可以修改的。下列代码就将ViewPort的大小,Scissor作为可动态修改的。

以下枚举内的都是可动态修改的。

typedef enum VkDynamicState {
    VK_DYNAMIC_STATE_VIEWPORT = 0,
    VK_DYNAMIC_STATE_SCISSOR = 1,
    VK_DYNAMIC_STATE_LINE_WIDTH = 2,
    VK_DYNAMIC_STATE_DEPTH_BIAS = 3,
    VK_DYNAMIC_STATE_BLEND_CONSTANTS = 4,
    VK_DYNAMIC_STATE_DEPTH_BOUNDS = 5,
    VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK = 6,
    VK_DYNAMIC_STATE_STENCIL_WRITE_MASK = 7,
    VK_DYNAMIC_STATE_STENCIL_REFERENCE = 8,
    VK_DYNAMIC_STATE_VIEWPORT_W_SCALING_NV = 1000087000,
    VK_DYNAMIC_STATE_DISCARD_RECTANGLE_EXT = 1000099000,
    VK_DYNAMIC_STATE_SAMPLE_LOCATIONS_EXT = 1000143000,
    VK_DYNAMIC_STATE_RAY_TRACING_PIPELINE_STACK_SIZE_KHR = 1000347000,
    VK_DYNAMIC_STATE_VIEWPORT_SHADING_RATE_PALETTE_NV = 1000164004,
    VK_DYNAMIC_STATE_VIEWPORT_COARSE_SAMPLE_ORDER_NV = 1000164006,
    VK_DYNAMIC_STATE_EXCLUSIVE_SCISSOR_NV = 1000205001,
    VK_DYNAMIC_STATE_FRAGMENT_SHADING_RATE_KHR = 1000226000,
    VK_DYNAMIC_STATE_LINE_STIPPLE_EXT = 1000259000,
    VK_DYNAMIC_STATE_CULL_MODE_EXT = 1000267000,
    VK_DYNAMIC_STATE_FRONT_FACE_EXT = 1000267001,
    VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY_EXT = 1000267002,
    VK_DYNAMIC_STATE_VIEWPORT_WITH_COUNT_EXT = 1000267003,
    VK_DYNAMIC_STATE_SCISSOR_WITH_COUNT_EXT = 1000267004,
    VK_DYNAMIC_STATE_VERTEX_INPUT_BINDING_STRIDE_EXT = 1000267005,
    VK_DYNAMIC_STATE_DEPTH_TEST_ENABLE_EXT = 1000267006,
    VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE_EXT = 1000267007,
    VK_DYNAMIC_STATE_DEPTH_COMPARE_OP_EXT = 1000267008,
    VK_DYNAMIC_STATE_DEPTH_BOUNDS_TEST_ENABLE_EXT = 1000267009,
    VK_DYNAMIC_STATE_STENCIL_TEST_ENABLE_EXT = 1000267010,
    VK_DYNAMIC_STATE_STENCIL_OP_EXT = 1000267011,
    VK_DYNAMIC_STATE_VERTEX_INPUT_EXT = 1000352000,
    VK_DYNAMIC_STATE_PATCH_CONTROL_POINTS_EXT = 1000377000,
    VK_DYNAMIC_STATE_RASTERIZER_DISCARD_ENABLE_EXT = 1000377001,
    VK_DYNAMIC_STATE_DEPTH_BIAS_ENABLE_EXT = 1000377002,
    VK_DYNAMIC_STATE_LOGIC_OP_EXT = 1000377003,
    VK_DYNAMIC_STATE_PRIMITIVE_RESTART_ENABLE_EXT = 1000377004,
    VK_DYNAMIC_STATE_COLOR_WRITE_ENABLE_EXT = 1000381000,
    VK_DYNAMIC_STATE_MAX_ENUM = 0x7FFFFFFF
} VkDynamicState;

下为imgui中使用的代码。

VkDynamicState dynamic_states[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR };
VkPipelineDynamicStateCreateInfo dynamic_state = {};
dynamic_state.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamic_state.dynamicStateCount = (uint32_t)IM_ARRAYSIZE(dynamic_states);
dynamic_state.pDynamicStates = dynamic_states;

创建管线

填充各种info然后创建pipeline。

VkGraphicsPipelineCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
info.flags = bd->PipelineCreateFlags;
info.stageCount = 2;
info.pStages = stage;
info.pVertexInputState = &vertex_info;
info.pInputAssemblyState = &ia_info;
info.pViewportState = &viewport_info;
info.pRasterizationState = &raster_info;
info.pMultisampleState = &ms_info;
info.pDepthStencilState = &depth_info;
info.pColorBlendState = &blend_info;
info.pDynamicState = &dynamic_state;
info.layout = bd->PipelineLayout;
info.renderPass = renderPass;
info.subpass = subpass;
VkResult err = vkCreateGraphicsPipelines(device, pipelineCache, 1, &info, allocator, pipeline);

渲染字体

// Use any command queue
// 这里的FrameIndex是窗口渲染管线的RenderPass的第二个FrameBuffer
VkCommandPool command_pool = wd->Frames[wd->FrameIndex].CommandPool;
VkCommandBuffer command_buffer = wd->Frames[wd->FrameIndex].CommandBuffer;

err = vkResetCommandPool(g_Device, command_pool, 0);
check_vk_result(err);
VkCommandBufferBeginInfo begin_info = {};
begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
begin_info.flags |= VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
// 从这里开始设置渲染命令
err = vkBeginCommandBuffer(command_buffer, &begin_info);
check_vk_result(err);
// 创建字体纹理,详细细节参见“文字渲染.md”
ImGui_ImplVulkan_CreateFontsTexture(command_buffer);

VkSubmitInfo end_info = {};
end_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
end_info.commandBufferCount = 1;
end_info.pCommandBuffers = &command_buffer;
err = vkEndCommandBuffer(command_buffer);
check_vk_result(err);
// 提交到队列
err = vkQueueSubmit(g_Queue, 1, &end_info, VK_NULL_HANDLE);
check_vk_result(err);
// 等待所有提交的命令执行完
err = vkDeviceWaitIdle(g_Device);
check_vk_result(err);
ImGui_ImplVulkan_DestroyFontUploadObjects();

字体纹理

以上是整体的结构,其中imgui的函数ImGui_ImplVulkan_CreateFontsTexture还蛮复杂,文字渲染.md文件里说明了这一过程。此过程主要是读取创建了文字的atlas并赋予给buffer、创建其Image、ImageView、DescriptorSet,而提交给Command Buffer的命令主要是将Buffer里的数据复制给Image。由于设置了MemoryBarrier,因此这个过程放在上列代码中不会有任何依赖&乱序&缓存不一致的问题。

进入MainLoop

glfwPollEvents()

获取events

设置Swap Chain大小

别忘了GLFW是一组和窗口相关的api。

if (g_SwapChainRebuild)
{
    int width, height;
    glfwGetFramebufferSize(window, &width, &height);
    if (width > 0 && height > 0)
    {
        ImGui_ImplVulkan_SetMinImageCount(g_MinImageCount);
        ImGui_ImplVulkanH_CreateOrResizeWindow(g_Instance, g_PhysicalDevice, g_Device, &g_MainWindowData, g_QueueFamily, g_Allocator, width, height, g_MinImageCount);
        g_MainWindowData.FrameIndex = 0;
        g_SwapChainRebuild = false;
    }
}

开始Dear ImGUI帧

// 将前一帧的backend给unuse
ImGui_ImplVulkan_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();

之后就是自己写一些窗口布局的代码。

渲染帧

获取Swap Chain中下一个Image

在一帧渲染开始前会调用 vkAcquireNextImageKHR 获得 swapchain 中的 image 索引,

When successful,vkAcquireNextImageKHRacquires a presentable image fromswapchainthat an application can use, and setspImageIndexto the index of that image within the swapchain.

但是这并不意味着我们可以立即开始渲染,因为

The presentation engine may not have finished reading from the image at the time it is acquired, so the application must usesemaphoreand/orfenceto ensure that the image layout and contents are not modified until the presentation engine reads have completed.

Image 可能还正在被读取,此时写入会破坏数据,因此必须要使用 semaphore 或 fence 保证读取完毕后再开始写入操作。

vkAcquireNextImageKHR(g_Device, wd->Swapchain, UINT64_MAX, image_acquired_semaphore, VK_NULL_HANDLE, &wd->FrameIndex);

通过获取Image的参数可知,其中使用到了之前创建的semaphore,在上一帧读取完之后才会立刻执行。

然后再等待Fences,暂时不知道为什么。

ImGui_ImplVulkanH_Frame* fd = &wd->Frames[wd->FrameIndex];
{
    err = vkWaitForFences(g_Device, 1, &fd->Fence, VK_TRUE, UINT64_MAX);    // wait indefinitely instead of periodically checking
    check_vk_result(err);

    err = vkResetFences(g_Device, 1, &fd->Fence);
    check_vk_result(err);
}

重置命令缓冲池开始录制命令缓冲

看起来每帧都需要重置Command Pool之后再操作Command Buffers。

Resetting a command pool recycles all of the resources from all of the command buffers allocated from the command pool back to the command pool. All command buffers that have been allocated from the command pool are put in the initial state.

也就是说,ResetCommandPool会将池中所有Command Buffer都重置,即将其状态改变为Initial State。

err = vkResetCommandPool(g_Device, fd->CommandPool, 0);
check_vk_result(err);
VkCommandBufferBeginInfo info = {};
info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
info.flags |= VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
err = vkBeginCommandBuffer(fd->CommandBuffer, &info);
check_vk_result(err);

其中vkBeginCommandBuffer会将Command Buffer的状态从“Initial State”变为“Recording State”,之后再调用vkCmd*之类的指令,就会记录到该Command Buffer里。

记录ImGUI中的Command

开始Render Pass
VkRenderPassBeginInfo info = {};
info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
info.renderPass = wd->RenderPass;
info.framebuffer = fd->Framebuffer;
info.renderArea.extent.width = wd->Width;
info.renderArea.extent.height = wd->Height;
info.clearValueCount = 1;
info.pClearValues = &wd->ClearValue;
vkCmdBeginRenderPass(fd->CommandBuffer, &info, VK_SUBPASS_CONTENTS_INLINE);

vkCmdBeginRenderPass到vkCmdEndRenderPass之间的代码会记录到RenderPass中,spec上说会记录到RenderPass的第一个SubPass里(不知道其他RenderPass怎么搞暂时)。

准备Vertex和Index数据(Buffer)

在需要用三角形进行渲染时会先创建Vertex和Index。

 // Create or resize the vertex/index buffers
 size_t vertex_size = draw_data->TotalVtxCount * sizeof(ImDrawVert);
 size_t index_size = draw_data->TotalIdxCount * sizeof(ImDrawIdx);
 if (rb->VertexBuffer == VK_NULL_HANDLE || rb->VertexBufferSize < vertex_size)
     CreateOrResizeBuffer(rb->VertexBuffer, rb->VertexBufferMemory, rb->VertexBufferSize, vertex_size, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
 if (rb->IndexBuffer == VK_NULL_HANDLE || rb->IndexBufferSize < index_size)
     CreateOrResizeBuffer(rb->IndexBuffer, rb->IndexBufferMemory, rb->IndexBufferSize, index_size, VK_BUFFER_USAGE_INDEX_BUFFER_BIT);

上面的CreateOrResizeBuffer又是ImGUI封装的函数,其函数定义如下:

它们分别创建了Vertex和Index Buffer。

 static void CreateOrResizeBuffer(VkBuffer& buffer, VkDeviceMemory& buffer_memory, VkDeviceSize& p_buffer_size, size_t new_size, VkBufferUsageFlagBits usage)
 {
     ImGui_ImplVulkan_Data* bd = ImGui_ImplVulkan_GetBackendData();
     ImGui_ImplVulkan_InitInfo* v = &bd->VulkanInitInfo;
     VkResult err;
     if (buffer != VK_NULL_HANDLE)
         vkDestroyBuffer(v->Device, buffer, v->Allocator);
     if (buffer_memory != VK_NULL_HANDLE)
         vkFreeMemory(v->Device, buffer_memory, v->Allocator);
 
     // ceil,奇怪的是,明明在创建字体Atlas的贴图时并没有考虑对齐的问题
     // 试了一下,其实不对齐也没有问题
     VkDeviceSize vertex_buffer_size_aligned = ((new_size - 1) / bd->BufferMemoryAlignment + 1) * bd->BufferMemoryAlignment;
     VkBufferCreateInfo buffer_info = {};
     buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
     buffer_info.size = vertex_buffer_size_aligned;
     // usage用来表明是Vertex还是Index之类
     buffer_info.usage = usage;
     buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
     // 创建Buffer
     err = vkCreateBuffer(v->Device, &buffer_info, v->Allocator, &buffer);
     check_vk_result(err);
 
     VkMemoryRequirements req;
     vkGetBufferMemoryRequirements(v->Device, buffer, &req);
     bd->BufferMemoryAlignment = (bd->BufferMemoryAlignment > req.alignment) ? bd->BufferMemoryAlignment : req.alignment;
     VkMemoryAllocateInfo alloc_info = {};
     alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
     alloc_info.allocationSize = req.size;
     alloc_info.memoryTypeIndex = ImGui_ImplVulkan_MemoryType(VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, req.memoryTypeBits);
     // 为Buffer分配内存
     err = vkAllocateMemory(v->Device, &alloc_info, v->Allocator, &buffer_memory);
     check_vk_result(err);
     // 将Buffer和内存绑定
     err = vkBindBufferMemory(v->Device, buffer, buffer_memory, 0);
     check_vk_result(err);
     p_buffer_size = req.size;
 }

在创建了Vertex和Index Buffer之后,就需要对其赋值。

 // Upload vertex/index data into a single contiguous GPU buffer
 ImDrawVert* vtx_dst = nullptr;
 ImDrawIdx* idx_dst = nullptr;
 VkResult err = vkMapMemory(v->Device, rb->VertexBufferMemory, 0, rb->VertexBufferSize, 0, (void**)(&vtx_dst));
 check_vk_result(err);
 err = vkMapMemory(v->Device, rb->IndexBufferMemory, 0, rb->IndexBufferSize, 0, (void**)(&idx_dst));
 check_vk_result(err);
 // 将主机内存中的相关数据拷贝到Buffer
 for (int n = 0; n < draw_data->CmdListsCount; n++)
 {
     const ImDrawList* cmd_list = draw_data->CmdLists[n];
     memcpy(vtx_dst, cmd_list->VtxBuffer.Data, cmd_list->VtxBuffer.Size * sizeof(ImDrawVert));
     memcpy(idx_dst, cmd_list->IdxBuffer.Data, cmd_list->IdxBuffer.Size * sizeof(ImDrawIdx));
     vtx_dst += cmd_list->VtxBuffer.Size;
     idx_dst += cmd_list->IdxBuffer.Size;
 }
 // 将Memory刷到GPU(大概) 有一个疑问,就是这个刷入的过程并不是cmd的,不知道会不会有同步问题
 VkMappedMemoryRange range[2] = {};
 range[0].sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE;
 range[0].memory = rb->VertexBufferMemory;
 range[0].size = VK_WHOLE_SIZE;
 range[1].sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE;
 range[1].memory = rb->IndexBufferMemory;
 range[1].size = VK_WHOLE_SIZE;
 err = vkFlushMappedMemoryRanges(v->Device, 2, range);
 check_vk_result(err);
 vkUnmapMemory(v->Device, rb->VertexBufferMemory);
 vkUnmapMemory(v->Device, rb->IndexBufferMemory);

记录Pipeline指令

调用封装过后的这个ImGUI函数。

ImGui_ImplVulkan_RenderDrawData(draw_data, fd->CommandBuffer);

巨复杂无比。

pipeline绑定

在里面,先进行了pipeline的绑定。spec说在绑定之后,会影响和对于pipeline类型的pipeline的子序列命令,直到有新的相同pipeline类型的pipeline绑定,也就是说在此之后的cmd都是作用到当前pipeline。

VK_PIPELINE_BIND_POINT_GRAPHICS这个enum是表明了pipeline类型,称为Binding Point,显然同一时刻只能有一个对应类型的pipeline能提交指令。

vkCmdBindPipeline(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
Buffer绑定

把上一步创建的Vertex和Index Buffer绑定到当前pipeline。

if (draw_data->TotalVtxCount > 0)
{
    VkBuffer vertex_buffers[1] = { rb->VertexBuffer };
    VkDeviceSize vertex_offset[1] = { 0 };
    vkCmdBindVertexBuffers(command_buffer, 0, 1, vertex_buffers, vertex_offset);
    vkCmdBindIndexBuffer(command_buffer, rb->IndexBuffer, 0, sizeof(ImDrawIdx) == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32);
}
创建ViewPort

viewport指明了片元如何从NDC坐标系转换到FrameBuffer的坐标系。

VkViewport viewport;
viewport.x = 0;
viewport.y = 0;
viewport.width = (float)fb_width;
viewport.height = (float)fb_height;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
vkCmdSetViewport(command_buffer, 0, 1, &viewport);
设置变换变量

通过PushConstants将变量设置到Pipeline Layout中。注意,这种方式的变量是写到command buffer里的,比在GPU里面创建Memory要快。

可以通过这里注意到,uniform变量其实是绑定到Pipeline Layout的。

float scale[2];
scale[0] = 2.0f / draw_data->DisplaySize.x;
scale[1] = 2.0f / draw_data->DisplaySize.y;
float translate[2];
translate[0] = -1.0f - draw_data->DisplayPos.x * scale[0];
translate[1] = -1.0f - draw_data->DisplayPos.y * scale[1];
vkCmdPushConstants(command_buffer, bd->PipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, sizeof(float) * 0, sizeof(float) * 2, scale);
vkCmdPushConstants(command_buffer, bd->PipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, sizeof(float) * 2, sizeof(float) * 2, translate);

这些变量可以通过以下方式在着色器中访问:

layout (location = 0) in vec2 scale;
layout (location = 1) in vec2 translate;

绘制DrawList

之前的,不管是窗口还是文字还是组件,他们都将顶点之类的信息添加到了DrawList里面,在此处一并绘制。

注:下文的Command List、Command Buffer、Command都是ImGUI的概念,而不是Vulkan中的概念

Command结构如下:注释里面说一个Command一般是一个DrawCall。

// Typically, 1 command = 1 GPU draw call (unless command is a callback)
// - VtxOffset: When 'io.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset' is enabled,
//   this fields allow us to render meshes larger than 64K vertices while keeping 16-bit indices.
//   Backends made for <1.71. will typically ignore the VtxOffset fields.
// - The ClipRect/TextureId/VtxOffset fields must be contiguous as we memcmp() them together (this is asserted for).
struct ImDrawCmd
{
    ImVec4          ClipRect;           // 4*4  // Clipping rectangle (x1, y1, x2, y2). Subtract ImDrawData->DisplayPos to get clipping rectangle in "viewport" coordinates
    ImTextureID     TextureId;          // 4-8  // User-provided texture ID. Set by user in ImfontAtlas::SetTexID() for fonts or passed to Image*() functions. Ignore if never using images or multiple fonts atlas.
    unsigned int    VtxOffset;          // 4    // Start offset in vertex buffer. ImGuiBackendFlags_RendererHasVtxOffset: always 0, otherwise may be >0 to support meshes larger than 64K vertices with 16-bit indices.
    unsigned int    IdxOffset;          // 4    // Start offset in index buffer.
    unsigned int    ElemCount;          // 4    // Number of indices (multiple of 3) to be rendered as triangles. Vertices are stored in the callee ImDrawList's vtx_buffer[] array, indices in idx_buffer[].
    ImDrawCallback  UserCallback;       // 4-8  // If != NULL, call the function instead of rendering the vertices. clip_rect and texture_id will be set normally.
    void*           UserCallbackData;   // 4-8  // The draw callback code can access this.

    ImDrawCmd() { memset(this, 0, sizeof(*this)); } // Also ensure our padding fields are zeroed

    // Since 1.83: returns ImTextureID associated with this draw call. Warning: DO NOT assume this is always same as 'TextureId' (we will change this function for an upcoming feature)
    inline ImTextureID GetTexID() const { return TextureId; }
};

绘制伪代码如下

对于每个CommandList:

​ 对于每个CommandList内的每个Command:

​ 如果Command有自定义回调函数,则调用回调函数并继续。 ​ 否则先将Scissor范围映射到FrameBuffer坐标系,并把渲染范围限定在Viewport内,然后设置Scissor(使用vkCmdSetScissor),试了一下如果没有Scissor限制范围,就可能会造成闪烁的情况。

// Project scissor/clipping rectangles into framebuffer space
ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y);
ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y);

// Clamp to viewport as vkCmdSetScissor() won't accept values that are off bounds
if (clip_min.x < 0.0f) { clip_min.x = 0.0f; }
if (clip_min.y < 0.0f) { clip_min.y = 0.0f; }
if (clip_max.x > fb_width) { clip_max.x = (float)fb_width; }
if (clip_max.y > fb_height) { clip_max.y = (float)fb_height; }
if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y)
    continue;

​ 然后将纹理绑定到DescriptorSet。读者可能注意到,前文已经创建、更新过不少descriptorset并进行过一些操作了,在此之前descriptorset都储存在host内存上,在这里再进行绑定,创建更新和绑定分离,能够进行复用,其实蛮好的。

// Bind DescriptorSet with font or user texture
VkDescriptorSet desc_set[1] = { (VkDescriptorSet)pcmd->TextureId };
if (sizeof(ImTextureID) < sizeof(ImU64))
{
    // We don't support texture switches if ImTextureID hasn't been redefined to be 64-bit. Do a flaky check that other textures haven't been used.
    IM_ASSERT(pcmd->TextureId == (ImTextureID)bd->FontDescriptorSet);
    desc_set[0] = bd->FontDescriptorSet;
}
vkCmdBindDescriptorSets(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, bd->PipelineLayout, 0, 1, desc_set, 0, nullptr);

​ 最后进行绘制。vkCmdDrawIndexed这个函数的第二个参数表示需要绘制的顶点总数,第三个是绘制的实例数,第四个是绘制所用的第一个index位于Index Buffer中的base offset,第五个是用到的第一个vertex处于Vertex Buffer中的offset,最后一个是用到的第一个instance的index。这就说明Index Buffer和Vertex Buffer都已经提交完毕了(虽然我暂时还没看到)。

vkCmdDrawIndexed(command_buffer, pcmd->ElemCount, 1, pcmd->IdxOffset + global_idx_offset, pcmd->VtxOffset + global_vtx_offset, 0);
结束Render Pass命令录制
vkCmdEndRenderPass(fd->CommandBuffer);

至此,已完成了界面渲染的命令记录。可见,对于每一个元素甚至每一个文字,都会提交一份顶点、索引(虽然顶点和索引是整体储存在两个Buffer里的)、纹理数据,一个元素就是一个drawcall

结束并提交Command Buffer

// Submit command buffer
vkCmdEndRenderPass(fd->CommandBuffer);
{
    // 输出颜色图
    VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    // submit的信息
    VkSubmitInfo info = {};
    info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    info.waitSemaphoreCount = 1;
    // 在执行这一系列指令开始时等待的semaphore
    info.pWaitSemaphores = &image_acquired_semaphore;
    info.pWaitDstStageMask = &wait_stage;
    info.commandBufferCount = 1;
    info.pCommandBuffers = &fd->CommandBuffer;
    // 在执行完毕后signal的semaphore
    info.signalSemaphoreCount = 1;
    info.pSignalSemaphores = &render_complete_semaphore;

    err = vkEndCommandBuffer(fd->CommandBuffer);
    check_vk_result(err);
    err = vkQueueSubmit(g_Queue, 1, &info, fd->Fence);
    check_vk_result(err);
}

更新窗口

调用这两个函数。

ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();

GLFW创建窗口

函数原型是static void ImGui_ImplGlfw_CreateWindow(ImGuiViewport* viewport)。看起来是通过Viewport来创建的Window。

使用glfwCreateWindow并在viewport data内储存了窗口指针,同时ImGUI的viewport也会储存窗口指针和windows handle。

为窗口创建回调函数

ImGUI接管GLFW的事件。

// Install GLFW callbacks for secondary viewports
glfwSetWindowFocusCallback(vd->Window, ImGui_ImplGlfw_WindowFocusCallback);
glfwSetCursorEnterCallback(vd->Window, ImGui_ImplGlfw_CursorEnterCallback);
glfwSetCursorPosCallback(vd->Window, ImGui_ImplGlfw_CursorPosCallback);
glfwSetMouseButtonCallback(vd->Window, ImGui_ImplGlfw_MouseButtonCallback);
glfwSetScrollCallback(vd->Window, ImGui_ImplGlfw_ScrollCallback);
glfwSetKeyCallback(vd->Window, ImGui_ImplGlfw_KeyCallback);
glfwSetCharCallback(vd->Window, ImGui_ImplGlfw_CharCallback);
glfwSetWindowCloseCallback(vd->Window, ImGui_ImplGlfw_WindowCloseCallback);
glfwSetWindowPosCallback(vd->Window, ImGui_ImplGlfw_WindowPosCallback);
glfwSetWindowSizeCallback(vd->Window, ImGui_ImplGlfw_WindowSizeCallback);

创建Surface渲染Window

创建平台相关的Surface。

而后检测硬件是否支持WSI。

// Check for WSI support
VkBool32 res;
vkGetPhysicalDeviceSurfaceSupportKHR(v->PhysicalDevice, v->QueueFamily, wd->Surface, &res);
if (res != VK_TRUE)
{
    IM_ASSERT(0); // Error: no WSI support on physical device
    return;
}
再指明格式和色彩空间。
// Select Surface Format
const VkFormat requestSurfaceImageFormat[] = { VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8_UNORM, VK_FORMAT_R8G8B8_UNORM };
const VkColorSpaceKHR requestSurfaceColorSpace = VK_COLORSPACE_SRGB_NONLINEAR_KHR;
wd->SurfaceFormat = ImGui_ImplVulkanH_SelectSurfaceFormat(v->PhysicalDevice, wd->Surface, requestSurfaceImageFormat, (size_t)IM_ARRAYSIZE(requestSurfaceImageFormat), requestSurfaceColorSpace);
设置呈现模式
// Select Present Mode
// FIXME-VULKAN: Even thought mailbox seems to get us maximum framerate with a single window, it halves framerate with a second window etc. (w/ Nvidia and SDK 1.82.1)
VkPresentModeKHR present_modes[] = { VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_FIFO_KHR };
wd->PresentMode = ImGui_ImplVulkanH_SelectPresentMode(v->PhysicalDevice, wd->Surface, &present_modes[0], IM_ARRAYSIZE(present_modes));
//printf("[vulkan] Secondary window selected PresentMode = %d\n", wd->PresentMode);
创建属于自定义GUI的SwapChain和Command Buffer

每个Image或者说FrameBuffer一个Command Buffer。

// Create SwapChain, RenderPass, Framebuffer, etc.
wd->ClearEnable = (viewport->Flags & ImGuiViewportFlags_NoRendererClear) ? false : true;
ImGui_ImplVulkanH_CreateOrResizeWindow(v->Instance, v->PhysicalDevice, v->Device, wd, v->QueueFamily, v->Allocator, (int)viewport->Size.x, (int)viewport->Size.y, v->MinImageCount);
vd->WindowOwned = true;

显示窗口

if (is_new_platform_window)
{
// On startup ensure new platform window don't steal focus (give it a few frames, as nested contents may lead to viewport being created a few frames late)
if (g.FrameCount < 3)
viewport->Flags |= ImGuiViewportFlags_NoFocusOnAppearing;

// Show window
g.PlatformIO.Platform_ShowWindow(viewport);

// Even without focus, we assume the window becomes front-most.
// This is useful for our platform z-order heuristic when io.MouseHoveredViewport is not available.
if (viewport->LastFrontMostStampCount != g.ViewportFrontMostStampCount)
viewport->LastFrontMostStampCount = ++g.ViewportFrontMostStampCount;
}

设置focus

// Update our implicit z-order knowledge of platform windows, which is used when the backend cannot provide io.MouseHoveredViewport.
// When setting Platform_GetWindowFocus, it is expected that the platform backend can handle calls without crashing if it doesn't have data stored.
// FIXME-VIEWPORT: We should use this information to also set dear imgui-side focus, allowing us to handle os-level alt+tab.
if (g.PlatformIO.Platform_GetWindowFocus != NULL)
{
    ImGuiViewportP* focused_viewport = NULL;
    for (int n = 0; n < g.Viewports.Size && focused_viewport == NULL; n++)
    {
        ImGuiViewportP* viewport = g.Viewports[n];
        if (viewport->PlatformWindowCreated)
            if (g.PlatformIO.Platform_GetWindowFocus(viewport))
                focused_viewport = viewport;
    }

    // Store a tag so we can infer z-order easily from all our windows
    // We compare PlatformLastFocusedViewportId so newly created viewports with _NoFocusOnAppearing flag
    // will keep the front most stamp instead of losing it back to their parent viewport.
    if (focused_viewport && g.PlatformLastFocusedViewportId != focused_viewport->ID)
    {
        if (focused_viewport->LastFrontMostStampCount != g.ViewportFrontMostStampCount)
            focused_viewport->LastFrontMostStampCount = ++g.ViewportFrontMostStampCount;
        g.PlatformLastFocusedViewportId = focused_viewport->ID;
    }
}

渲染窗口

大的要来了!

void ImGui::RenderPlatformWindowsDefault(void platform_render_arg, void renderer_render_arg)这个函数是默认的渲染窗口的函数,修改了ImGUI的渲染方式后可以自定义此渲染函数。

//    ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
//    for (int i = 1; i < platform_io.Viewports.Size; i++)
//        if ((platform_io.Viewports[i]->Flags & ImGuiViewportFlags_Minimized) == 0)
//            MyRenderFunction(platform_io.Viewports[i], my_args);
//    for (int i = 1; i < platform_io.Viewports.Size; i++)
//        if ((platform_io.Viewports[i]->Flags & ImGuiViewportFlags_Minimized) == 0)
//            MySwapBufferFunction(platform_io.Viewports[i], my_args);

此函数会对每个viewport调用如下函数:

if (platform_io.Platform_RenderWindow) platform_io.Platform_RenderWindow(viewport, platform_render_arg);
if (platform_io.Renderer_RenderWindow) platform_io.Renderer_RenderWindow(viewport, renderer_render_arg);

platform_io.Platform_RenderWindow函数其实就是将glfw的上下文设置为viewport对应的window。

而platform_io.Renderer_RenderWindow函数是真正进行渲染的。

获取下一个Image

err = vkAcquireNextImageKHR(v->Device, wd->Swapchain, UINT64_MAX, fsd->ImageAcquiredSemaphore, VK_NULL_HANDLE, &wd->FrameIndex);
check_vk_result(err);
fd = &wd->Frames[wd->FrameIndex];

自旋等待Fences

err = vkWaitForFences(v->Device, 1, &fd->Fence, VK_TRUE, 100);
if (err == VK_SUCCESS) break;
if (err == VK_TIMEOUT) continue;
check_vk_result(err);

重置并开始录制Command Buffer

err = vkResetCommandPool(v->Device, fd->CommandPool, 0);
check_vk_result(err);
VkCommandBufferBeginInfo info = {};
info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
info.flags |= VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
err = vkBeginCommandBuffer(fd->CommandBuffer, &info);
check_vk_result(err);

绑定RenderPass并开始录制

使用交换链显示窗口

先进行platform_io.Platform_SwapBuffers,但好像什么都没执行。

if (platform_io.Platform_SwapBuffers) platform_io.Platform_SwapBuffers(viewport, platform_render_arg);
if (platform_io.Renderer_SwapBuffers) platform_io.Renderer_SwapBuffers(viewport, renderer_render_arg);

然后platform_io.Renderer_SwapBuffers。

需要先创建PresentInfo,而PresentInfo里面包含了Semaphore。

ImGui_ImplVulkanH_FrameSemaphores* fsd = &wd->FrameSemaphores[wd->SemaphoreIndex];
VkPresentInfoKHR info = {};
info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
info.waitSemaphoreCount = 1;
// vkQueuePresentKHR发出wait的指令,直到RenderCompleteSemaphore被管线渲染完毕才会执行此指令
info.pWaitSemaphores = &fsd->RenderCompleteSemaphore;
info.swapchainCount = 1;
// 设置SwapChain
info.pSwapchains = &wd->Swapchain;
// 设置用作输出的Image
info.pImageIndices = &present_index;
err = vkQueuePresentKHR(v->Queue, &info);

执行完vkQueuePresentKHR,我们的窗口就得到了更新!

一些概念

Image | ImageView | FrameBuffer | RenderTarget | Attachment

图片信息真正的储存位置是VkMemory,VkDeviceMemory,以字节形式存储。 而Image首先包含了一些色彩格式之类的元信息,然后提供了使用Texel进行访问RGBA的方法。 ImageView就是提供了以不同view来读取Image信息的方法。例如提供了reinterpret方法,可以将原本的RGBA给reinterpret成FT(F=RG, T=BA)、读取Image指定mip的数据、读取Image的指定array(没大懂)。

Attachment分为Color和Depth,但Attachment正如其名,只是一个附件,只储存了一些元信息。

FrameBuffer将RenderPass和对应的Attachment绑定到一起。

RenderTarget是一个Subpass渲染的结果,但是好像在Vulkan api里并不存在RT的概念,而是通过Attachments来实现的,一个Subpass会接受来自上一个Subpass的Attachments作为输入,然后又会输出计算后的Attachments。

FrameBuffers

K0NRD

Command Buffer状态

这篇文章写得蛮不错(但文字功底是真的拉)。

下面聊一下command buffer的生命周期,每个command buffer都一定存在于其中一个状态中:

  • Initial:当创建一个command buffer的时候,它的初始状态就是initial state。一些命令可以将一个command buffer(或一组command buffers)从executable、recording、invalid状态置回该状态。initial 状态的command buffers只能被moved to recording状态,或者释放;
  • RecordingvkBeginCommandBuffer将command buffer的状态从initial 状态切换到 recording状态。一旦command buffer处于recording状态,则可以通过vkCmd*命令来对该command buffer进行录制;
  • ExecutablevkEndCommandBuffer用于结束一个command buffer的录制,并将其从recording state 切换到 executable state。executable commdn buffers可以被 submitted、reset或者 recorded to another command buffer;
  • Pending:Queue submission of a command buffer将command buffer的状态从executable state切换到pending state。当在pending state的时候,应用程序不得以任何方式修改command buffer————因为device可能正在处理command buffer中录制的command。一旦command buffer执行完毕,该状态将会被revert到executable state(如果该command buffer录制的时候打了 VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT flag,则会被move to invalid state)。a synchronization command should被用于监测这个事情的发生;
  • Invalid:一些操作,比如:修改或者删除command buffer中某个command所使用的资源,都会导致command buffer的状态切换到invalid state。invalid state的command buffer只能被reset或者释放。 在这里插入图片描述

在command buffer上操作任何command,都有其自身的要求,它要求command buffer必须处于什么状态,这在该command 的有效使用限制中有详细说明。

reset 一个command buffer 将丢弃先前录制的command,并将command buffer置于initial state。 reset是调用 vkResetCommandBuffervkResetCommandPool、或者vkBeginCommandBuffer的一部分(该命令还会使得command buffer处于录制状态)。

Secondary command buffers也通过vkCmdExecuteCommands被记录到primary command buffer中。这将两个command buffer的生命周期联系在一起——如果primary command buffer被submit到queue,那么primary command buffer以及其关联的所有secondary command buffer都将move to pending state。一旦primary command buffer执行完毕,其关联的secondary command buffer也就都执行完毕了。当每个command buffer都执行完毕后,它们将分别进入它们相应的完成状态(如上所述,executable state或者invalid state)。

当一个secondary command buffer move to invalid state或者initial state,那么它关联的所有primary command buffer都将move to invalid state。而primary command buffer切换状态,不会影响secondary command buffer的状态。(需要注意的是:如果reset或者释放一个primary command buffer,会删除所有关联的secondary command buffer的lifecycle linkage)。

Command buffer是从command pool中获取内存创建的,这样的话,可以将多个command buffer的创建成本平摊。command pool是externally synchronized,意味着一个command pool不能被多个线程同时使用。这包括:pool中创建的command buffer录制命令,以及分配、释放和重置command buffer或者pool本身。