在 WSL 里把 Vulkan 跑起来
原创 · 约 64 分钟阅读 · 阅读 --

在 WSL 里把 Vulkan 跑起来

作者: Alex Xiang

图形编程工程实践

Vulkan 的第一个三角形出了名地麻烦。创建窗口之前要理解 Instance,选显卡之前要枚举 Physical Device,真正提交绘制命令之前还要经过 Logical Device、Queue、Swapchain、Pipeline、Command Buffer 和同步对象。换到 WSL,前面又多了一层:Linux 程序看到的显卡,未必对应一套原生 Linux Vulkan 驱动。

这并不意味着 WSL 不适合学 Vulkan。相反,它很适合搭建一套干净的 C++、CMake 和 Shader 工具链,也方便同时观察软件实现与硬件实现。只是开始写代码以前,必须先回答三个问题:

  1. Vulkan loader 能不能找到驱动?
  2. 找到的是 GPU,还是 CPU 上的软件实现?
  3. 这个实现真正支持到哪个 Vulkan 版本?

本文用一台 WSL2 + Ubuntu 26.04 测试环境,从零散的系统组件开始,实际跑通了设备枚举和 vkcube。硬件路径能识别 NVIDIA 独显和 Intel 核显,软件路径也能独立运行。下面把完整过程整理成一条可以复现的学习路线。

在 WSL 中开发并运行 Vulkan 程序

先说结论:WSL 能学,但要知道自己跑在哪

在原生 Linux 中,Vulkan 应用通常经过 loader,进入 NVIDIA、AMD 或 Intel 提供的 Vulkan 驱动,最后到达 GPU。WSL 的图形路径不同。微软文档说明,WSL 的 GPU 访问通过 /dev/dxg 转发到 Windows GPU;WSLg 则负责把 Linux 的 X11/Wayland 窗口集成到 Windows 桌面。

对 Vulkan 来说,Ubuntu 的 Mesa 可能提供两条很有用的路径:

  • Dzn(Dozen):Mesa 的 Vulkan-on-D3D12 驱动。Linux Vulkan 调用被转换成 D3D12,再交给 Windows 显卡驱动。它可以使用真实 GPU,但当前环境会明确提示它不是 conformant Vulkan implementation。
  • Lavapipe(LVP):Mesa 的 CPU Vulkan 实现。它不依赖真实 GPU,速度远不如硬件,却很适合验证 loader、对象生命周期和无窗口测试。

WSL 中 Vulkan 的 Dzn 硬件路径与 Lavapipe 软件路径

这两条路径不是“正确答案”和“错误答案”的关系。

路径实际执行位置适合做什么主要限制
Dzn → D3D12 → GPUWindows 管理的真实 GPU窗口、基本渲染、日常练习功能与兼容性受 Dzn/Mesa 版本约束,不适合做最终兼容性结论
Lavapipe → CPUWSL CPUCI、设备枚举、API 验证、无 GPU 排错很慢,不能代表 GPU 性能
原生 Windows VulkanWindows 厂商 Vulkan 驱动性能分析、厂商工具、最终验证构建环境不再是纯 Linux
原生 Linux VulkanLinux 厂商 Vulkan 驱动Linux 发布与性能验证需要双系统、独立主机或完整虚拟机直通

我的建议是:用 WSL 学概念和写代码,用 Dzn 看画面,用 Lavapipe 做对照;涉及性能、扩展支持和发布兼容性时,再到原生 Windows 或原生 Linux 复测。

Vulkan 里到底有哪些层

第一次接触 Vulkan,最容易把 loader、SDK、validation layer 和 driver 混成一件事。实际上,它们职责不同:

应用程序

  ├─ Vulkan headers:声明类型与函数

  ├─ Vulkan loader:发现并加载驱动,分发 Vulkan 调用
  │    │
  │    ├─ Validation Layer:开发期可选,检查错误用法
  │    │
  │    └─ ICD/Driver:真正实现 Vulkan

  └─ Shader 工具:把 GLSL/HLSL/Slang 编译成 SPIR-V

libvulkan-dev 主要提供开发头文件和链接库,libvulkan1 提供 loader。安装这两个包,并不意味着机器已经有可工作的 GPU Vulkan 驱动。反过来,Windows 里的 NVIDIA 驱动和 CUDA 可用,也不能证明 WSL 里的 Linux Vulkan 程序能直接加载 NVIDIA 原生 ICD。

Khronos 的 loader 会扫描驱动 manifest,Linux 常见位置包括:

/etc/vulkan/icd.d/
/usr/share/vulkan/icd.d/

每个 JSON 文件指向一个驱动共享库,例如:

{
  "ICD": {
    "api_version": "1.4.348",
    "library_path": "libvulkan_lvp.so"
  },
  "file_format_version": "1.0.1"
}

这里的 api_version 也不是最终答案。loader 文档特别提醒:manifest 写的是驱动可支持的最高 API 版本,底层物理设备实际支持什么,仍要通过 vkGetPhysicalDeviceProperties 查询。

准备 WSL 和 Windows

Windows 侧先做三件事

使用 Windows 11 或支持 WSLg 的较新 Windows 10。在 PowerShell 中执行:

wsl --update
wsl --list --verbose
wsl --status

目标是确认发行版使用 WSL 2。更新完成后可以重启 WSL:

wsl --shutdown

显卡驱动应从 NVIDIA、AMD 或 Intel 的官方渠道安装在 Windows 宿主机。不要为了 WSL 图形开发,再在 Linux 发行版里安装一套完整的桌面版 NVIDIA 内核驱动。WSL 使用的是 Windows 提供的 GPU 虚拟化通道。

WSL 侧检查 vGPU 和 WSLg

进入 Ubuntu 后先检查:

uname -a
test -e /dev/dxg && echo "vGPU device exists"

printf 'DISPLAY=%s\n' "$DISPLAY"
printf 'WAYLAND_DISPLAY=%s\n' "$WAYLAND_DISPLAY"
printf 'XDG_RUNTIME_DIR=%s\n' "$XDG_RUNTIME_DIR"

正常的 WSLg 环境通常能看到:

/dev/dxg
DISPLAY=:0
WAYLAND_DISPLAY=wayland-0
XDG_RUNTIME_DIR=/run/user/1000

/dev/dxg 存在说明 GPU 调用可以转发到 Windows;DISPLAYWAYLAND_DISPLAY 则决定 GUI 程序能否创建窗口。二者不是同一件事:有 GPU 设备但窗口环境损坏时,计算程序可能正常,vkcube 仍然打不开。

如果是 NVIDIA 显卡,还可以执行:

nvidia-smi

它能证明 CUDA/管理通道识别到了 GPU,但仍然不能替代 vulkaninfo

安装一套够学习使用的工具链

Ubuntu 仓库中的包已经足够完成前几阶段学习:

sudo apt update
sudo apt install -y \
  build-essential \
  cmake \
  ninja-build \
  pkg-config \
  libvulkan-dev \
  vulkan-tools \
  vulkan-validationlayers \
  glslc \
  glslang-tools \
  spirv-tools \
  libglfw3-dev \
  libglm-dev

这些工具分别解决不同问题:

用途
libvulkan-devVulkan C/C++ 头文件与 loader 链接库
vulkan-toolsvulkaninfovkcube
vulkan-validationlayersVK_LAYER_KHRONOS_validation
glslc / glslang-tools把 Shader 编译成 SPIR-V
spirv-tools校验、反汇编、优化 SPIR-V
libglfw3-dev创建窗口并获得 WSI 扩展
libglm-dev向量、矩阵与变换
cmake / ninja-build构建工程

LunarG 曾提供 Ubuntu APT 仓库,但其官网已经注明:2025 年 5 月以后不再为新 SDK 持续更新 Ubuntu Packages。如果需要与最新版 Vulkan SDK 完全一致,应使用 LunarG 的 Linux tarball;如果目标是学习 Vulkan 1.1/1.2 基础和 WSL 排错,Ubuntu 自带包通常更省事。

最新版 Khronos Vulkan Tutorial 已经采用 Vulkan 1.4、C++20、Vulkan-Hpp RAII、Dynamic Rendering、Timeline Semaphore 和 Slang。这个方向值得学习,但不要一上来就假设 WSL 的 Dzn 同样支持 Vulkan 1.4。本文实测的 Dzn 设备只报告 Vulkan 1.2,初期代码应把目标版本设在实际设备能力以内。

第一次体检:loader 找到了谁

先不要运行示例工程,直接执行:

vulkaninfo --summary

输出很长时,只看四项:

  1. Vulkan Instance Version
  2. deviceName
  3. deviceType
  4. driverName

本文测试机的默认驱动集合里同时存在 Dzn 和 Lavapipe。为了避免“到底跑在哪”变成猜谜,可以让 loader 只加载指定 manifest。

新版 loader 推荐使用 VK_DRIVER_FILES

VK_DRIVER_FILES=/usr/share/vulkan/icd.d/dzn_icd.json \
  vulkaninfo --summary

实测硬件路径返回:

GPU0:
  apiVersion = 1.2.348
  deviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU
  deviceName = Microsoft Direct3D12 (NVIDIA GeForce RTX 4060 Laptop GPU)
  driverName = Dozen

GPU1:
  apiVersion = 1.2.348
  deviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU
  deviceName = Microsoft Direct3D12 (Intel(R) UHD Graphics)
  driverName = Dozen

同时会看到警告:

WARNING: dzn is not a conformant Vulkan implementation, testing use only.

这条警告不等于“完全不能用”,但不能忽略。它的准确含义是:当前 Dzn 实现没有通过对应版本的 Khronos 一致性认证。学习基础对象和渲染流程可以继续;遇到扩展、边界行为或性能问题时,不应直接归因于自己的代码。

再单独测试 Lavapipe:

VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json \
  vulkaninfo --summary

实测输出:

apiVersion = 1.4.348
deviceType = PHYSICAL_DEVICE_TYPE_CPU
deviceName = llvmpipe (LLVM 20.1.8, 256 bits)
driverName = llvmpipe

这里有一个很有意思的现象:软件驱动报告 Vulkan 1.4,而硬件路径只报告 1.2。版本号更高不代表更快。 它只说明实现对 API/特性的支持范围,性能还要看 deviceType 和实际执行设备。

再用 vkcube 验证窗口和交换链

设备枚举成功只证明 Instance 和 Physical Device 可用。vkcube 还会覆盖 Surface、Swapchain、Queue、Shader、Command Buffer 和 Present:

VK_DRIVER_FILES=/usr/share/vulkan/icd.d/dzn_icd.json \
  vkcube --wsi xcb

如果 WSLg、Dzn 和窗口系统正常,会弹出旋转立方体,并在终端看到类似:

Selected GPU 0: Microsoft Direct3D12 (...), type: DiscreteGpu

这是本次测试中由 vkcube 真正提交并呈现的一帧,不是为了文章重新绘制的示意图:

WSL Dzn 路径下实际运行的 vkcube 帧

截图对应的实际输出为:

Selected GPU 0: Microsoft Direct3D12 (NVIDIA GeForce RTX 4060 Laptop GPU),
type: DiscreteGpu

软件路径也可以测试:

VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json \
  vkcube --wsi xcb

它应选择 llvmpipe,窗口能出现,但 CPU 占用和帧率不能与硬件路径相比。

本文附带的程序不是伪代码

为了避免“代码看起来合理,读者复制后却跑不起来”,本文同时提供了一个完整的 CMake 工程:

源码仓库:github.com/ax2/wsl-vulkan-basics

wsl-vulkan-basics/
├── CMakeLists.txt
├── README.md
├── shaders/
│   ├── triangle.vert
│   └── triangle.frag
└── src/
    ├── device_probe.cpp
    └── triangle.cpp

它会生成两个程序:

程序覆盖的 Vulkan 路径
vk-probeInstance、Physical Device 枚举与属性读取
vk-triangleGLFW/X11、Surface、Swapchain、Pipeline、Command Buffer、同步与 Present

安装依赖后,可直接构建和测试:

git clone https://github.com/ax2/wsl-vulkan-basics.git
cd wsl-vulkan-basics
cmake -S . -B build -G Ninja
cmake --build build
ctest --test-dir build --output-on-failure

构建不是只把 C++ 编译通过:CMake 会调用 glslc 生成 SPIR-V,并紧接着用 spirv-val 校验两个 Shader。少装了工具、Shader 编译失败或 SPIR-V 非法,构建会当场失败。

写第一段 Vulkan 代码:只做设备枚举

不要把“第一个程序”直接定成三角形。先写一个只创建 Instance、枚举 Physical Device、打印属性的小程序。它只有一条资源生命周期:

vkCreateInstance
  -> vkEnumeratePhysicalDevices
  -> vkGetPhysicalDeviceProperties
  -> vkDestroyInstance

新建 main.cpp

#include <vulkan/vulkan.h>

#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

static void check(VkResult result, const char* operation) {
    if (result != VK_SUCCESS) {
        throw std::runtime_error(
            std::string(operation) + " failed: " + std::to_string(result)
        );
    }
}

int main() {
    VkApplicationInfo appInfo{};
    appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
    appInfo.pApplicationName = "wsl-vulkan-probe";
    appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
    appInfo.pEngineName = "none";
    appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
    appInfo.apiVersion = VK_API_VERSION_1_1;

    VkInstanceCreateInfo createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
    createInfo.pApplicationInfo = &appInfo;

    VkInstance instance = VK_NULL_HANDLE;
    check(vkCreateInstance(&createInfo, nullptr, &instance), "vkCreateInstance");

    uint32_t count = 0;
    check(
        vkEnumeratePhysicalDevices(instance, &count, nullptr),
        "count physical devices"
    );

    std::vector<VkPhysicalDevice> devices(count);
    check(
        vkEnumeratePhysicalDevices(instance, &count, devices.data()),
        "enumerate physical devices"
    );

    std::cout << "physical_devices=" << count << '\n';
    for (uint32_t i = 0; i < count; ++i) {
        VkPhysicalDeviceProperties properties{};
        vkGetPhysicalDeviceProperties(devices[i], &properties);
        std::cout
            << '[' << i << "] " << properties.deviceName
            << " | Vulkan "
            << VK_VERSION_MAJOR(properties.apiVersion) << '.'
            << VK_VERSION_MINOR(properties.apiVersion) << '.'
            << VK_VERSION_PATCH(properties.apiVersion)
            << '\n';
    }

    vkDestroyInstance(instance, nullptr);
    return EXIT_SUCCESS;
}

如果只想单独练习这个探测程序,最小 CMakeLists.txt 如下。随文工程使用的是包含 Shader 和三角形程序的完整版本:

cmake_minimum_required(VERSION 3.20)
project(wsl_vulkan_probe LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

find_package(Vulkan REQUIRED)

add_executable(vk-probe main.cpp)
target_link_libraries(vk-probe PRIVATE Vulkan::Vulkan)
target_compile_options(vk-probe PRIVATE -Wall -Wextra -Wpedantic)

构建并分别运行:

cmake -S . -B build -G Ninja
cmake --build build

VK_DRIVER_FILES=/usr/share/vulkan/icd.d/dzn_icd.json \
  ./build/vk-probe

VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json \
  ./build/vk-probe

本文实际得到:

# Dzn
physical_devices=2
[0] Microsoft Direct3D12 (NVIDIA GeForce RTX 4060 Laptop GPU) | Vulkan 1.2.348
[1] Microsoft Direct3D12 (Intel(R) UHD Graphics) | Vulkan 1.2.348

# Lavapipe
physical_devices=1
[0] llvmpipe (LLVM 20.1.8, 256 bits) | Vulkan 1.4.348

这段程序很短,却把以后排错最重要的边界固定下来了:应用创建的 API 版本、loader 发现的驱动、驱动暴露的物理设备,是三层不同的信息。

Shader 不在运行时临时猜

Vulkan 消费的是 SPIR-V。初学时可以从 GLSL 开始,分别写顶点 Shader 和片元 Shader。

triangle.vert

#version 450

layout(location = 0) out vec3 fragColor;

vec2 positions[3] = vec2[](
    vec2( 0.0, -0.6),
    vec2( 0.6,  0.6),
    vec2(-0.6,  0.6)
);

vec3 colors[3] = vec3[](
    vec3(1.0, 0.2, 0.2),
    vec3(0.2, 1.0, 0.3),
    vec3(0.2, 0.4, 1.0)
);

void main() {
    gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
    fragColor = colors[gl_VertexIndex];
}

triangle.frag

#version 450

layout(location = 0) in vec3 fragColor;
layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(fragColor, 1.0);
}

编译、校验并反汇编:

glslc triangle.vert -o triangle.vert.spv
glslc triangle.frag -o triangle.frag.spv

spirv-val triangle.vert.spv
spirv-val triangle.frag.spv
spirv-dis triangle.vert.spv | less

把 Shader 编译做成 CMake 的显式构建步骤,而不是每次手工敲命令:

find_program(GLSLC glslc REQUIRED)

foreach(SHADER triangle.vert triangle.frag)
  add_custom_command(
    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${SHADER}.spv
    COMMAND ${GLSLC}
            ${CMAKE_CURRENT_SOURCE_DIR}/${SHADER}
            -o ${CMAKE_CURRENT_BINARY_DIR}/${SHADER}.spv
    DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${SHADER}
    VERBATIM
  )
  list(APPEND SPIRV_FILES ${CMAKE_CURRENT_BINARY_DIR}/${SHADER}.spv)
endforeach()

add_custom_target(shaders ALL DEPENDS ${SPIRV_FILES})
add_dependencies(vk-probe shaders)

Khronos 的新教程转向 Slang 是合理的,但 GLSL + glslc 仍然适合第一次建立“源码 → SPIR-V → Shader Module”的心智模型。等三角形跑通,再比较 GLSL、HLSL 和 Slang,会比一开始同时学习三种语法有效得多。

把完整三角形真正跑起来

随文工程的 src/triangle.cpp 不是省略错误处理的伪代码。它完成了设备和队列选择、Surface 与 Swapchain 创建、Shader Module、Render Pass、Graphics Pipeline、Command Pool、Command Buffer、Fence、Semaphore 和 Present 的完整生命周期。

用 WSL 的 Dzn 路径启动:

cd wsl-vulkan-basics
VK_DRIVER_FILES=/usr/share/vulkan/icd.d/dzn_icd.json \
  ./build/vk-triangle

同一份程序也经过 Lavapipe 实测:

VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json \
  ./build/vk-triangle

下面这张图来自程序第三次 vkQueuePresentKHR 提交后的真实 Swapchain 图像。捕获过程使用 Mesa 的 VK_LAYER_MESA_screenshot,没有用绘图软件重建画面:

本文完整 Vulkan 示例程序实际输出的 RGB 三角形

想复现截图,可以执行:

mkdir -p captures

VK_DRIVER_FILES=/usr/share/vulkan/icd.d/dzn_icd.json \
VK_INSTANCE_LAYERS=VK_LAYER_MESA_screenshot \
VK_LAYER_MESA_SCREENSHOT_CONFIG="frames=3,output_dir=$PWD/captures" \
  ./build/vk-triangle

第三帧会保存为 captures/3.png。与截桌面相比,这种方法直接截取 Present 前的交换链图像,窗口遮挡和桌面缩放不会影响结果。

一张三角形也会暴露真实问题

这个示例不是一次写对的,实际运行中遇到了三个很典型的问题。

第一处发生在窗口创建前。GLFW 自动探测 Wayland 时触发了 WSLg/GDK 错误,程序尚未进入 Vulkan。示例在 GLFW 3.4 及以上版本显式选择 X11;Ubuntu 24.04 自带的 GLFW 3.3 没有这组宏,因此用编译期版本判断保持兼容:

#if GLFW_VERSION_MAJOR > 3 || \
    (GLFW_VERSION_MAJOR == 3 && GLFW_VERSION_MINOR >= 4)
glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11);
#endif

第二处是程序能运行,但捕获到的第一帧全黑。Shader、Swapchain 和 Present 都没有报错,真正原因是 Vulkan 视口坐标下的顶点绕序与 frontFace 设置相反,三角形被背面剔除了:

rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;

第三处只有 Validation Layer 能可靠发现。最初所有交换链图像共用一个 renderFinished Semaphore;某一帧进入 Present 后,CPU 可能在 Present 尚未结束时再次把它作为 signal semaphore 提交。错误对应:

VUID-vkQueueSubmit-pSignalSemaphores-00067

最终实现为每张交换链图像创建独立的 render-finished Semaphore,并按 imageIndex 使用:

render_finished_.resize(swapchain_images_.size());

submit_info.pSignalSemaphores = &render_finished_[image_index];
present_info.pWaitSemaphores = &render_finished_[image_index];

修复后,Dzn 与 Lavapipe 两条路径都能持续渲染;再以 VK_LAYER_KHRONOS_validation 运行 5 秒,日志中的 Validation ErrorVUID-* 数量均为 0。Dzn 自身的 “not a conformant Vulkan implementation” 警告仍会出现,那是实现声明,不是这个程序的 API 使用错误。

第一个三角形真正需要哪些对象

Vulkan 难学,不是因为每个函数都难,而是对象多、对象之间有依赖。建议按依赖顺序理解,不要按头文件顺序背 API。

Vulkan 对象的推荐学习顺序

1. Instance:应用与 Vulkan 世界的连接

Instance 决定应用启用哪些 instance extension 和 layer。窗口库 GLFW 会告诉你当前平台创建 Surface 所需的扩展,不要把 XCB、Xlib 或 Wayland 扩展硬编码进跨平台程序。

2. Physical Device:选择能力,不是创建资源

Physical Device 代表可选 GPU/软件设备。选择时至少检查:

  • Queue Family 是否支持 graphics;
  • 是否支持向目标 Surface present;
  • 是否支持 VK_KHR_swapchain
  • Surface format 和 present mode 是否可用;
  • 需要的 feature 是否真的支持。

WSL 中同时出现 NVIDIA 和 Intel Dzn 设备时,不要依赖枚举顺序。应给设备打分,或者允许用命令行指定。

3. Logical Device 与 Queue:把能力变成可提交的通道

Logical Device 是对选中 Physical Device 的使用实例。创建时明确申请 Queue 和 Feature。Graphics、Compute、Transfer 可能共享一个 Queue Family,也可能分开。

初学阶段不要为了“高级”强行找独立 transfer queue。先用一条 graphics queue 跑通,再测量是否值得拆分。

4. Surface、Swapchain 与 Pipeline:画到哪里,怎么画

Surface 是窗口系统与 Vulkan 的接口。Swapchain 是一组可轮换显示的图像。Graphics Pipeline 则固定了 Shader、输入装配、光栅化、多重采样、颜色混合等状态。

新教程使用 Dynamic Rendering,省去了传统 VkRenderPass/VkFramebuffer 的预声明,但并没有消除图像布局、附件格式和同步要求。Dzn 只报告 Vulkan 1.2 时,需要检查 VK_KHR_dynamic_rendering 扩展,而不是默认它是 core feature。

5. Command Buffer 与同步:GPU 不按 CPU 的直觉执行

绘制命令先记录到 Command Buffer,再提交到 Queue。Queue 提交通常是异步的;函数返回不代表 GPU 已经画完。

一帧最小流程是:

Vulkan 一帧中的 Acquire、Record、Submit 与 Present

Acquire swapchain image
  -> 等待图像可用
  -> Record/Reuse command buffer
  -> Submit graphics work
  -> 等待渲染完成
  -> Present

常见同步对象的职责:

对象谁等待谁典型用途
Binary SemaphoreQueue/Present 等 GPU 操作图像可用、渲染完成
Timeline Semaphore带递增数值的 GPU/CPU 时间线多阶段、多帧或异步计算
FenceCPU 等待 GPU控制 frames in flight,安全复用资源
Pipeline BarrierGPU 阶段之间的内存与执行依赖图像布局转换、写后读

初学时最危险的写法是每帧 vkDeviceWaitIdle。它可能让画面“看起来正确”,同时把并行性全部抹掉。正确练习应该是两到三帧 in flight,每帧一组 Fence/Semaphore,并明确资源归属。

Validation Layer 要从第一天打开

Vulkan 为了减少驱动开销,核心 API 不替你做大量合法性检查。Khronos 官方文档明确建议:开发阶段启用 VK_LAYER_KHRONOS_validation,发布时关闭。

先确认 layer 已安装:

vulkaninfo | grep VK_LAYER_KHRONOS_validation

代码中还应启用 VK_EXT_debug_utils,注册 Debug Messenger,只输出 Warning 和 Error:

debugInfo.messageSeverity =
    VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT |
    VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;

debugInfo.messageType =
    VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
    VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
    VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;

Validation Layer 能抓到很多肉眼难以定位的问题:

  • 对象销毁顺序错误;
  • Image Layout 不匹配;
  • Descriptor 已失效;
  • Command Buffer 仍在执行就被重置;
  • 多线程同时访问不允许共享的对象;
  • Queue Family ownership 没有正确转移;
  • Pipeline stage/access mask 组合不合法。

错误消息里通常带 VUID-*。不要只搜索整段英文报错,优先复制 VUID 到 Vulkan Specification 中查对应的 Valid Usage 条件。

RenderDoc、NVIDIA Nsight 等工具很重要,但顺序应该是:先让 Validation Layer 干净,再做帧捕获。 否则捕获工具展示的是一个已经违反 API 约束的帧,分析结果可能没有意义。

WSL 中最常见的故障怎么拆

vkCreateInstance 返回 VK_ERROR_INCOMPATIBLE_DRIVER

先看 loader 是否发现 manifest:

find /etc/vulkan/icd.d /usr/share/vulkan/icd.d \
  -maxdepth 1 -type f -name '*.json' -print 2>/dev/null

再打开 loader 调试日志:

VK_LOADER_DEBUG=error,warn,driver vulkaninfo --summary

不要一上来复制别人的 .so 文件。manifest、共享库架构、loader 版本和依赖必须匹配。

只看到 llvmpipe

这表示 loader 找到了 Lavapipe,但没有找到或无法初始化硬件路径。依次检查:

test -e /dev/dxg
ls /usr/share/vulkan/icd.d/dzn_icd.json
VK_DRIVER_FILES=/usr/share/vulkan/icd.d/dzn_icd.json \
  vulkaninfo --summary

如果 Dzn manifest 不存在,需要检查发行版提供的 mesa-vulkan-drivers 是否包含 Dzn,而不是盲目设置 GPU 环境变量。

vkcube 报无法创建 Surface

先验证:

echo "$DISPLAY"
echo "$WAYLAND_DISPLAY"
ls -la /mnt/wslg

然后分别尝试:

vkcube --wsi xcb
vkcube --wsi wayland

如果 WSLg 本身异常,在 Windows 执行 wsl --updatewsl --shutdown 后重试。不要先改 Vulkan 代码。

Dzn 警告 non-conformant

这是实现属性,不是你的程序发出的 Validation Error。保留警惕但不必停止所有学习。遇到下列情况应换原生环境复测:

  • 依赖较新的 Vulkan 1.3/1.4 core feature;
  • 使用 ray tracing、mesh shader 等扩展;
  • 需要准确 GPU 性能数据;
  • 出现只在 Dzn 上复现的渲染错误;
  • 准备发布 Linux 或 Windows 正式程序。

VK_DRIVER_FILES 设置了却没有效果

不要用 sudo 启动 Vulkan 开发程序。出于安全原因,loader 在提权场景会忽略 VK_DRIVER_FILESVK_LAYER_PATH 等用户可控变量。

旧资料常使用 VK_ICD_FILENAMES。它目前仍兼容,但 Khronos loader 文档已经将其标记为 deprecated,新脚本应改用 VK_DRIVER_FILES

窗口能开,但画面是黑的

这通常已经不是 WSL 问题。按下面顺序查:

  1. Validation Layer 是否有 Error;
  2. Shader Module 是否来自正确的 SPIR-V;
  3. Pipeline 的颜色格式是否与 Swapchain 一致;
  4. Viewport/Scissor 是否覆盖窗口;
  5. Image Layout 是否转换正确;
  6. Command Buffer 是否真的提交;
  7. Present 是否等待 render-finished semaphore;
  8. 窗口缩放后是否重建 Swapchain。

把“WSL 环境”和“Vulkan 状态机”分开排查,效率会高很多。

一条更有效的学习路线

Vulkan 的资料很多,按教程顺序抄完并不等于掌握。每一阶段都应该有可观察的输出和一个故意制造的错误。

阶段一:建立设备与 loader 心智模型

完成本文的 vk-probe

  • 打印 Instance Version;
  • 枚举 Physical Device;
  • 打印 Queue Family;
  • 打印扩展与 Feature;
  • 分别指定 Dzn 和 Lavapipe。

故意把 apiVersion 设得高于设备支持范围,观察返回值和 loader 日志。

阶段二:画出三角形

按依赖顺序实现:

Window
→ Instance
→ Surface
→ Physical Device
→ Logical Device/Queues
→ Swapchain
→ Shader Modules
→ Graphics Pipeline
→ Command Buffers
→ Sync Objects
→ Frame Loop

不要急着封装“大而全 Renderer”。每成功一个对象,就给它明确的析构位置。三角形阶段结束时,Validation Layer 应当零 Error。

阶段三:把固定三角形变成资源系统

依次增加:

  • Vertex Buffer;
  • Index Buffer;
  • Staging Buffer;
  • Uniform Buffer;
  • Descriptor Set;
  • Texture/Image/Sampler;
  • Depth Buffer;
  • Mipmap。

每增加一种资源,都回答四个问题:谁创建、内存在哪里、何时可读写、谁负责销毁。

阶段四:处理真实窗口和多帧

加入:

  • 窗口 resize;
  • Swapchain 重建;
  • 多帧 in flight;
  • Fence/Semaphore;
  • 帧时间统计;
  • 设备选择参数;
  • Validation 开关。

这一阶段完成后,程序才从“演示代码”开始接近可维护应用。

阶段五:做一个小项目

比起继续堆 API,更推荐完成一个边界清楚的小项目:

  • glTF 模型查看器;
  • 粒子系统;
  • Mandelbrot/Julia 集合计算与显示;
  • 简单 PBR 场景;
  • GPU 图像滤镜;
  • Compute Shader 布料或流体练习。

项目中至少加入 CMake、资源目录、Shader 增量编译、Validation、截图测试和一份设备能力报告。

阶段六:再读现代 Vulkan

基础稳定后,再系统学习:

  • Vulkan 1.3/1.4 feature;
  • Dynamic Rendering;
  • Synchronization2;
  • Timeline Semaphore;
  • Descriptor Indexing/Descriptor Buffer;
  • Pipeline Cache;
  • Ray Tracing;
  • Vulkan Memory Allocator;
  • RenderDoc/Nsight;
  • 多平台 Surface 与发布。

这时再看 Khronos 的 Simple Engine 和 Vulkan Samples,会比第一次就从引擎架构开始更容易理解。

WSL 学习环境应该如何验收

最后给自己留一张检查表:

[ ] /dev/dxg 存在
[ ] DISPLAY 或 WAYLAND_DISPLAY 正常
[ ] vulkaninfo --summary 能运行
[ ] 能明确区分 Dzn 和 Lavapipe
[ ] Dzn 能识别目标 GPU
[ ] vkcube 能创建窗口并持续渲染
[ ] CMake 能找到 Vulkan::Vulkan
[ ] glslc 能生成 SPIR-V
[ ] spirv-val 校验通过
[ ] vk-probe 能在两种 ICD 下运行
[ ] Validation Layer 可启用
[ ] 第一个三角形无 Validation Error

达到这些条件,WSL 就已经是一套可用的 Vulkan 学习环境。它不是原生 GPU 驱动的替代品,也不应该拿来做最终性能结论;但对于理解 Vulkan 对象、编写跨平台 C++、练习 Shader、建立验证和构建流程,它足够好,而且两条执行路径反而提供了一个难得的对照实验。

参考资料

打开原图 ↗