Vulkan学习笔记
在过去,我以为Vulkan只有高端安卓才会用,相当复杂,没有下定决心去学。后来发现很多桌面和主机游戏也在用Vulkan,Vulkan真的很出色很重要,于是这几天开始看英伟达nvpro和Vulkan官方教程,打算自己敲一个小demo
我的Vulkan项目
最近搓了一个Vulkan项目,记录一下学习过程

Vulkan API
Queue
参考 what-is-actually-a-queue-family-in-vulkan
Queue是将命令提交到GPU的入口,Command buffer会被提交到Queue中按顺序执行
你可以使用多个线程分别提交命令到多个Queue中
提交到不同Queue的命令,其执行顺序不确定,但可以使用Semaphore进行同步
Vulkan的命令有:
- vkCmdDraw
- vkCmdDispatch:执行Compute Shader
- vkCmdCopy
- vkQueueBindSparse
通常一个Queue只能处理某几种命令
一个硬件往往只有有限个Queue
Render Pass
画到哪
Vulkan相较于OpenGL、DX11/12,一大特点就是有Render Pass这个概念
Render Pass的作用是描述绘制的目标(attachments,很像RT),比如color,比如depth
比如一个Forward Pass,他的目标可能就是一张color和一张depth,一个Deferred Pass,他的目标可能是GBuffer(一组color)和一个depth
VkAttachmentDescription
值得注意的参数
- format:image view格式
- samples:多采样次数
- finalLayout:当renderpass绘制结束时,会将color转化为该格式
- 画向Swapchain Image,finalLayout配置为
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
- 给其他pass的shader采样,设置为
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
Subpass
一个像素在绘制单元绘制后,通常需要被拷贝出去,形成一张完整的RT,然后再由另一个绘制单元绘制
Subpass的一大特点是一个像素被一个Subpass绘制后,并不会立刻被拷贝出去,而是继续被另一个Subpass绘制
优点:
- 减少了拷贝带来的带宽,减少延迟和发热,移动端延迟渲染常用该技术
- 像素间绘制彼此独立,不需要一整张RT绘制后再绘制下一张,提高并行效率,还能用来实现OIT和Zero Overdraw
缺点:
- 拿不到其他位置的RT的绘制内容,无法实现一些依赖RT的屏幕后效
Pipeline
怎么画
用于设置管线的状态
| 参数 |
主要作用 |
| 管线类型 |
图形管线还是计算管线 |
| Shader Stage |
指定绘制用的Shader |
| Vertex Input |
顶点缓冲的结构 |
| Input Assembly |
输入的结构,比如三角形的拓扑规则 |
| Viewport & Scissors |
|
| Rasterizer |
光栅化的规则,比如用线、用三角面绘制,是否背面剔除 |
| Multisampling |
多采样的规则 |
| Depth Stencil |
深度测试、模板测试的规则 |
| Blending |
颜色混合规则(半透明) |
| Pipeline Layout |
管线的结构,比如UBO binding规则 |
在Vulkan中,Render Pass是比Pipeline大的,这也很好理解,毕竟只要绘制目标没有改变,自然不需要改动Render Pass
而一个场景中有大量不同材质、Shader的对象,而且有的可能是线,有的是三角形,于是需要经常调整Pipeline,于是Vulkan的Pipeline就像Descriptor Set一样,可以随时改绑定的
Vulkan的Pipeline是很大的东西,实时创建大量开销过大,而大部分的Pipeline对象都很接近,可能就只有一小部分有差异。为了实现复用,Vulkan的Pipeline是可以动态修改、可以Cache、可以生成子类
Descriptor Set Layout
Shader中的资源位置
Shader中会使用很多资源,比如UBO,比如贴图Sampler,需要指定绑定的位置
layout(binding = 0) uniform GlobalUBO { mat4 modelMatrix; mat4 viewMatrix; mat4 projectionMatrix; } ubo;
|
layout(binding = 1) uniform sampler2D texSampler;
|
Descriptor Set
资源
简单理解为Vulkan管线能理解、使用的资源,比如UBO,比如贴图Sampler
在绘制时,我们可以绑定一个Descriptor Sets,里面包含多个Descriptor Set,其结构是Layout决定的
通常我们为每一个材质实例创建一个Descriptor Set(一个UBO或者贴图可以Update到多个Descriptor Set上)
我们可以单独更新某一个Set
| VkDescriptorType |
含义 |
类似DX12中的 |
| VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE |
读图片 |
SRV |
| VK_DESCRIPTOR_TYPE_STORAGE_IMAGE |
读写图片 |
UAV |
| VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER |
读图片,并包含各种采样配置 |
SRV + Sampler |
| VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER |
常量缓冲 |
UBO |
Image
Texure
Usage
图片的使用目的
| VkImageUsageFlagBits |
含义 |
| VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
Color渲染目标 |
| VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT |
深度缓冲、模板缓冲渲染目标 |
| VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
图像拷贝的源 |
| VK_IMAGE_USAGE_TRANSFER_DST_BIT |
图形拷贝的目标 |
| VK_IMAGE_USAGE_SAMPLED_BIT |
可以被着色器读 |
| VK_IMAGE_USAGE_STORAGE_BIT |
可以被着色器写 |
Layout
图形在内存的布局和排列,会影响拷贝、渲染等行为的可用性
| VkImageLayout |
用途 |
| VK_IMAGE_LAYOUT_UNDEFINED |
图形初始化 |
| VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL |
Color渲染目标 |
| VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL |
深度缓冲、模板缓冲渲染目标 |
| VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL |
Shader可读的贴图 |
Image的Layout转化需要使用Pipeline Barrier
Image View
Fence
用于隔离不同帧

Semaphore
用于隔离渲染和呈现

Shader
Vulkan使用SPIR-V作为着色语言,这是一种底层的二进制着色语言,可以使用glslang编译GLSL得到
$glslangValidator -V vert_shader.vert
$glslangValidator -V vert_shader.vert -o test.spv
|
可以使用spirv-dis查看一个spv文件的内容(如果编译时带有调试信息,可以看到源码)
Vertex
#version 450
layout(binding = 0) uniform GlobalUBO { mat4 modelMatrix; mat4 viewMatrix; mat4 projectionMatrix; } ubo;
layout(location = 0) in vec3 inPosition; layout(location = 1) in vec3 inColor; layout(location = 2) in vec2 inTexCoord;
layout(location = 0) out vec3 fragColor; layout(location = 1) out vec2 uv0;
void main() { gl_Position = ubo.projectionMatrix * ubo.viewMatrix * ubo.modelMatrix * vec4(inPosition, 1.0); fragColor = inColor; uv0 = inTexCoord; }
|
Fragment
#version 450 layout(location = 0) in vec3 fragColor; layout(location = 1) in vec2 uv0;
layout(location = 0) out vec4 outColor;
layout(binding = 1) uniform sampler2D texSampler;
void main() { outColor = texture(texSampler, uv0); }
|
集成到cmake
在VS文件夹中显示
Visual Studio安装glsl插件后,shader可以高亮和智能提示
file(GLOB_RECURSE HEADERS "*.h") file(GLOB_RECURSE SOURCES "*.cpp")
set(SHADER_DIR "../Shader/Source") file(GLOB VERT_SHADERS "${SHADER_DIR}/*.vert") file(GLOB FRAG_SHADERS "${SHADER_DIR}/*.frag") set(ALL_FILES ${HEADERS} ${SOURCES})
add_library(Engine STATIC ${ALL_FILES} ${VERT_SHADERS} ${FRAG_SHADERS}) target_include_directories(Engine PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${ALL_FILES})
source_group("Shaders" FILES ${VERT_SHADERS} ${FRAG_SHADERS})
|
项目构建时编译
set(SPV_TARGET_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../Shader/SPV")
foreach(SHADER_FILE ${VERT_SHADERS}) get_filename_component(FILE_NAME ${SHADER_FILE} NAME) string(REPLACE "." "_" OUTPUT_STRING ${FILE_NAME}) message("Compile vert shader: ${FILE_NAME} => ${OUTPUT_STRING}.spv") add_custom_command(TARGET Engine POST_BUILD COMMAND glslangValidator -V -S vert ${SHADER_FILE} -o ${SPV_TARGET_DIR}/${OUTPUT_STRING}.spv ) endforeach()
foreach(SHADER_FILE ${FRAG_SHADERS}) get_filename_component(FILE_NAME ${SHADER_FILE} NAME) string(REPLACE "." "_" OUTPUT_STRING ${FILE_NAME}) message("Compile frag shader: ${FILE_NAME} => ${OUTPUT_STRING}.spv") add_custom_command(TARGET Engine POST_BUILD COMMAND glslangValidator -V -S frag ${SHADER_FILE} -o ${SPV_TARGET_DIR}/${OUTPUT_STRING}.spv ) endforeach()
|
add_custom_command(TARGET Client POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/Shader/SPV $<TARGET_FILE_DIR:Client>/Shaders)
|
ImGui
Vulkan ImGui本质是使用Vulkan API画平面,ImGui会帮你创建一个Pipeline,但是你需要自己准备Render Pass、Frame Buffer、Vulkan Context等内容
ImGui版本选择
ImGui不同分支功能不同,其中最多人使用的docking分支,这个分支下ImGui可以用来做Editor
Vulkan初始化
核心是需要一个Render Pass,这个Render Pass需要有一个Color Attachment,我建议单独开一个pass
ImGui初始化
可以认为这个过程是帮你创建以Render Pipeline
ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); (void)io; io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; setStyle();
ImGui_ImplGlfw_InitForVulkan(rhi_context.m_window.getHandle(), true); ImGui_ImplVulkan_InitInfo init_info = {}; init_info.Instance = rhi_context.m_instance->getHandle(); init_info.PhysicalDevice = rhi_context.m_device->getPhysicalDevice(); init_info.Device = rhi_context.m_device->getLogicalDevice(); init_info.QueueFamily = rhi_context.m_device->getGraphicsFamilyIndex(); init_info.Queue = rhi_context.m_device->getGraphicsQueue(); init_info.DescriptorPool = rhi_context.m_descriptor_pool->getHandle(); init_info.MinImageCount = rhi_context.m_swapchain->getImageCount(); init_info.ImageCount = rhi_context.m_swapchain->getImageCount(); ImGui_ImplVulkan_Init(&init_info, render_pass.getHandle());
|
绘制
需要传入一个command buffer
ImGui_ImplVulkan_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); ImGui::DockSpaceOverViewport(nullptr, ImGuiDockNodeFlags_PassthruCentralNode);
ImGui::Begin("Hierarchy");
ImGui::End();
ImGui::Render();
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), cmd.getHandle(frame_index));
|
销毁
ImGui_ImplVulkan_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext();
|
Vulkan拓展
可以通过函数指针的方式引入一些Vulkan拓展
Debug Label
在使用RenderDoc截帧时,我们可以看到一些绘制命令被分类命名,一些贴图也有调试名称
PFN_vkCmdBeginDebugUtilsLabelEXT vkCmdBeginDebugUtilsLabelEXT = nullptr; PFN_vkCmdEndDebugUtilsLabelEXT vkCmdEndDebugUtilsLabelEXT = nullptr;
void RHIDevice::loadExtensionFunctions() { vkCmdBeginDebugUtilsLabelEXT = reinterpret_cast<PFN_vkCmdBeginDebugUtilsLabelEXT>(vkGetInstanceProcAddr(m_instance.getHandle(), "vkCmdBeginDebugUtilsLabelEXT")); vkCmdEndDebugUtilsLabelEXT = reinterpret_cast<PFN_vkCmdEndDebugUtilsLabelEXT>(vkGetInstanceProcAddr(m_instance.getHandle(), "vkCmdEndDebugUtilsLabelEXT")); }
void RHIDevice::beginDebugUtilsLabel(VkCommandBuffer cmd, const VkDebugUtilsLabelEXT& label) { vkCmdBeginDebugUtilsLabelEXT(cmd, &label); }
void RHIDevice::endDebugUtilsLabel(VkCommandBuffer cmd) { vkCmdEndDebugUtilsLabelEXT(cmd); }
|