在 WSL 里把 Vulkan 跑起来

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 能学,但要知道自己跑在哪

在原生 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、对象生命周期和无窗口测试。

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

路径 实际执行位置 适合做什么 主要限制
Dzn → D3D12 → GPU Windows 管理的真实 GPU 窗口、基本渲染、日常练习 功能与兼容性受 Dzn/Mesa 版本约束,不适合做最终兼容性结论
Lavapipe → CPU WSL CPU CI、设备枚举、API 验证、无 GPU 排错 很慢,不能代表 GPU 性能
原生 Windows Vulkan Windows 厂商 Vulkan 驱动 性能分析、厂商工具、最终验证 构建环境不再是纯 Linux
原生 Linux Vulkan Linux 厂商 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-dev Vulkan C/C++ 头文件与 loader 链接库
vulkan-tools vulkaninfovkcube
vulkan-validationlayers VK_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 真正提交并呈现的一帧,不是为了文章重新绘制的示意图:

截图对应的实际输出为:

复制代码
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-probe Instance、Physical Device 枚举与属性读取
vk-triangle GLFW/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,没有用绘图软件重建画面:

想复现截图,可以执行:

复制代码
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。

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 已经画完。

一帧最小流程是:

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

常见同步对象的职责:

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

初学时最危险的写法是每帧 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、建立验证和构建流程,它足够好,而且两条执行路径反而提供了一个难得的对照实验。

查看原文

参考资料

相关推荐
Better Bench9 天前
WSL2 Ubuntu 中 Claude CLI “command not found” 故障排查与修复
linux·ubuntu·claude·wsl·claudecode
圣殿骑士-Khtangc15 天前
WSL2 + Redis Stack 完整部署教程(Windows 11)
windows·redis·wsl
Imagine Miracle25 天前
【WSL】让WSL2后台持久运行不自动关闭的解决方案
linux·windows·wsl
zhou135284822671 个月前
Linux 系统安装 Ollama 详细教程
linux·ubuntu·wsl·ollama
西游音月1 个月前
Windows环境下的WSL+Claude Code安装配置
windows·大模型·ai编程·wsl·claude code
随风一样自由1 个月前
【WSL+Linux】国内网络连接微软官方服务器(或GitHub)不稳定,导致下载内核或Linux发行版时超时断开如何解决?
linux·服务器·wsl
假如梵高是飞行员1 个月前
WSL2 从 img 镜像文件启动特定 Linux 发行版完整指南
linux·windows·wsl
Qres8212 个月前
docker & WSL & Ubuntu安装记录
ubuntu·docker·容器·wsl
weixin_ab2 个月前
【WSL2 + PgVector 远程连接排坑记】
wsl