第82章:Vulkan:集成Vulkan图形API
Vulkan,说实话,刚接触时我是有点发怵的。毕竟它不像OpenGL那样有状态机帮你兜底,一切都要自己管。但用久了你会发现——这种「裸奔」式的设计,反而让性能掌控变得无比清晰。
这一章,我们就来聊聊怎么在CMake项目里把Vulkan集成进来。我会结合自己踩过的坑,给你一套靠谱的集成方案。
Vulkan项目的基本结构
一个典型的Vulkan项目,通常包含这几样东西:
- Vulkan SDK:头文件、库文件、验证层
- glslangValidator:把GLSL着色器编译成SPIR-V
- Vulkan Memory Allocator(可选):管理显存的好帮手
我个人习惯把Vulkan SDK安装在固定路径,然后用CMake的find_package来找它。但要注意——Vulkan官方并没有提供标准的CMake Config文件,所以我们需要自己写查找逻辑。
CMake中查找Vulkan
CMake从3.7开始内置了FindVulkan模块。用法很简单:
find_package(Vulkan REQUIRED)
if(Vulkan_FOUND)
include_directories(${Vulkan_INCLUDE_DIRS})
target_link_libraries(my_app ${Vulkan_LIBRARIES})
endif()
嗯,这里要注意:Vulkan_LIBRARIES通常只包含vulkan-1.lib(Windows)或libvulkan.so(Linux)。但如果你用了验证层、窗口系统扩展,还得额外链接。
C:/VulkanSDK/,但CMake的FindVulkan模块不一定能自动找到。我建议手动设置VULKAN_SDK环境变量,或者在CMake命令行里传-DVULKAN_SDK=xxx。
集成着色器编译
Vulkan用SPIR-V作为着色器中间格式。你不能直接把GLSL源码扔给GPU,得先编译。我推荐在CMake里用add_custom_command来自动化这一步:
# 找到glslangValidator
find_program(GLSLC glslangValidator REQUIRED)
# 编译着色器
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/shaders/vert.spv
${CMAKE_CURRENT_BINARY_DIR}/shaders/frag.spv
COMMAND ${GLSLC} -V ${CMAKE_CURRENT_SOURCE_DIR}/shaders/shader.vert
-o ${CMAKE_CURRENT_BINARY_DIR}/shaders/vert.spv
COMMAND ${GLSLC} -V ${CMAKE_CURRENT_SOURCE_DIR}/shaders/shader.frag
-o ${CMAKE_CURRENT_BINARY_DIR}/shaders/frag.spv
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/shaders/shader.vert
${CMAKE_CURRENT_SOURCE_DIR}/shaders/shader.frag
COMMENT "Compiling Vulkan shaders..."
)
# 把生成的SPIR-V文件加入项目
add_custom_target(shaders DEPENDS
${CMAKE_CURRENT_BINARY_DIR}/shaders/vert.spv
${CMAKE_CURRENT_BINARY_DIR}/shaders/frag.spv
)
add_dependencies(my_app shaders)
这样做的好处是:你改一下着色器源码,重新构建时CMake会自动重新编译SPIR-V。我在项目里一直这么用,省心不少。
处理平台相关的窗口扩展
Vulkan本身不关心窗口,你得自己创建表面(Surface)。不同平台用的扩展不一样:
| 平台 | 扩展名称 | 所需库 |
|---|---|---|
| Windows | VK_KHR_win32_surface | vulkan-1.lib |
| Linux (X11) | VK_KHR_xlib_surface | X11, xcb |
| Linux (Wayland) | VK_KHR_wayland_surface | wayland-client |
| macOS | VK_MVK_macos_surface | MoltenVK |
在CMake里,我习惯用条件判断来链接不同平台的库:
if(WIN32)
target_link_libraries(my_app vulkan-1)
elseif(APPLE)
find_library(MOLTENVK MoltenVK)
target_link_libraries(my_app ${MOLTENVK})
elseif(UNIX)
find_package(X11 REQUIRED)
target_link_libraries(my_app ${X11_LIBRARIES})
target_include_directories(my_app PRIVATE ${X11_INCLUDE_DIR})
endif()
完整的CMakeLists.txt示例
下面是一个完整的示例,把上面所有内容串起来:
cmake_minimum_required(VERSION 3.10)
project(VulkanExample)
# 查找Vulkan
find_package(Vulkan REQUIRED)
# 查找着色器编译器
find_program(GLSLC glslangValidator REQUIRED)
# 添加可执行文件
add_executable(vulkan_example main.cpp)
# 链接Vulkan库
target_link_libraries(vulkan_example ${Vulkan_LIBRARIES})
target_include_directories(vulkan_example PRIVATE ${Vulkan_INCLUDE_DIRS})
# 平台相关处理
if(WIN32)
target_link_libraries(vulkan_example vulkan-1)
elseif(APPLE)
find_library(MOLTENVK MoltenVK REQUIRED)
target_link_libraries(vulkan_example ${MOLTENVK})
elseif(UNIX)
find_package(X11 REQUIRED)
target_link_libraries(vulkan_example ${X11_LIBRARIES})
target_include_directories(vulkan_example PRIVATE ${X11_INCLUDE_DIR})
endif()
# 着色器编译
set(SHADER_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/shaders/shader.vert
${CMAKE_CURRENT_SOURCE_DIR}/shaders/shader.frag
)
set(SHADER_OUTPUTS
${CMAKE_CURRENT_BINARY_DIR}/shaders/vert.spv
${CMAKE_CURRENT_BINARY_DIR}/shaders/frag.spv
)
add_custom_command(
OUTPUT ${SHADER_OUTPUTS}
COMMAND ${CMAKE_COMMAND} -E make_directory
${CMAKE_CURRENT_BINARY_DIR}/shaders
COMMAND ${GLSLC} -V ${CMAKE_CURRENT_SOURCE_DIR}/shaders/shader.vert
-o ${CMAKE_CURRENT_BINARY_DIR}/shaders/vert.spv
COMMAND ${GLSLC} -V ${CMAKE_CURRENT_SOURCE_DIR}/shaders/shader.frag
-o ${CMAKE_CURRENT_BINARY_DIR}/shaders/frag.spv
DEPENDS ${SHADER_SOURCES}
COMMENT "Compiling Vulkan shaders..."
)
add_custom_target(shaders ALL DEPENDS ${SHADER_OUTPUTS})
add_dependencies(vulkan_example shaders)
# 复制着色器到构建目录(方便运行时加载)
file(COPY ${SHADER_OUTPUTS} DESTINATION ${CMAKE_BINARY_DIR}/shaders/)
Vulkan集成知识体系
下面这张图,是我对Vulkan集成流程的总结。你看一眼就能明白整体脉络:
验证层的集成
Vulkan的验证层是调试利器。它能帮你检查API调用是否正确,比如有没有忘记创建描述符集、有没有提交了未初始化的命令缓冲区。
在CMake里,我通常这样处理验证层:
# 仅在Debug模式下启用验证层
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(my_app PRIVATE VULKAN_DEBUG=1)
# 验证层库通常随SDK一起提供,不需要额外链接
# 但需要在代码中显式加载
endif()
然后在C++代码里,根据这个宏来决定是否启用验证层:
#ifdef VULKAN_DEBUG
const std::vector<const char*> validationLayers = {
"VK_LAYER_KHRONOS_validation"
};
#else
const std::vector<const char*> validationLayers = {};
#endif
关于Vulkan Memory Allocator
Vulkan的内存管理比OpenGL复杂得多。你要自己分配显存、绑定内存、处理内存类型。说实话,手动做这些事很容易出错。
我建议集成Vulkan Memory Allocator(VMA)。它是一个单头文件库,用CMake集成非常简单:
# 下载vk_mem_alloc.h放到项目中
# 然后在CMake里:
target_include_directories(my_app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/third_party/VMA)
# 在其中一个源文件中定义实现
# #define VMA_IMPLEMENTATION
# #include "vk_mem_alloc.h"
VMA能帮你自动管理显存碎片、处理内存类型选择。我在一个需要频繁分配释放显存的项目里用了它,性能提升很明显。
常见问题与避坑
- 找不到Vulkan SDK:检查
VULKAN_SDK环境变量是否设置,或者手动指定路径。 - 着色器编译失败:确认glslangValidator版本与Vulkan SDK匹配。我曾经因为版本不匹配,编译出的SPIR-V在运行时崩溃。
- 链接错误:Windows上记得链接
vulkan-1.lib,而不是vulkan.lib。前者是动态库导入库,后者是静态库。 - 验证层报错:先确认你安装了Vulkan SDK的Validation Layers组件。如果还报错,检查代码中是否启用了不支持的扩展。
好了,Vulkan的CMake集成就聊到这里。这套方案我在多个项目里验证过,从Windows到Linux再到macOS,都能稳定运行。你按这个流程走,应该不会出大问题。
公众号:蓝海资料掘金营,微信deep3321