第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)。但如果你用了验证层、窗口系统扩展,还得额外链接。

我曾经踩过的坑:在Windows上,Vulkan SDK默认安装到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()
小提示:如果你用GLFW或SDL2,它们已经帮你封装了窗口创建和Vulkan表面。这种情况下,你只需要链接GLFW/SDL2,不用手动处理平台扩展。我个人强烈推荐GLFW,省事。

完整的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集成知识体系 Vulkan SDK find_package(Vulkan) 着色器编译 (glslangValidator) 平台窗口扩展处理 生成 .spv 文件 链接平台库 (X11, MoltenVK...) 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 SDK的开发机上有效。发布给用户时,记得关掉验证层,否则程序会崩溃(因为用户机器上没有验证层库)。

关于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