抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Vulkan学习笔记

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

我的Vulkan项目

最近搓了一个Vulkan项目,记录一下学习过程

2024_3_20

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绘制

优点:

  1. 减少了拷贝带来的带宽,减少延迟和发热,移动端延迟渲染常用该技术
  2. 像素间绘制彼此独立,不需要一整张RT绘制后再绘制下一张,提高并行效率,还能用来实现OIT和Zero Overdraw

缺点:

  1. 拿不到其他位置的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

用于隔离不同帧

fence

Semaphore

用于隔离渲染和呈现

semaphore

Shader

Vulkan使用SPIR-V作为着色语言,这是一种底层的二进制着色语言,可以使用glslang编译GLSL得到

# 将vert_shader.vert编译为vert_spv
$glslangValidator -V vert_shader.vert
# 将vert_shader.vert编译为test.spv
$glslangValidator -V vert_shader.vert -o test.spv

可以使用spirv-dis查看一个spv文件的内容(如果编译时带有调试信息,可以看到源码)

spirv-dis test.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})

项目构建时编译

# Compile 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()
# Copy Shader
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(); // 3
init_info.ImageCount = rhi_context.m_swapchain->getImageCount(); // 3;
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");
// draw something
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);
}

评论