在 WSL 里把 Vulkan 跑起来
Vulkan 的第一个三角形出了名地麻烦。创建窗口之前要理解 Instance,选显卡之前要枚举 Physical Device,真正提交绘制命令之前还要经过 Logical Device、Queue、Swapchain、Pipeline、Command Buffer 和同步对象。换到 WSL,前面又多了一层:Linux 程序看到的显卡,未必对应一套原生 Linux Vulkan 驱动。
这并不意味着 WSL 不适合学 Vulkan。相反,它很适合搭建一套干净的 C++、CMake 和 Shader 工具链,也方便同时观察软件实现与硬件实现。只是开始写代码以前,必须先回答三个问题:
- Vulkan loader 能不能找到驱动?
- 找到的是 GPU,还是 CPU 上的软件实现?
- 这个实现真正支持到哪个 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;DISPLAY 或 WAYLAND_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 | vulkaninfo、vkcube |
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
输出很长时,只看四项:
Vulkan Instance VersiondeviceNamedeviceTypedriverName
本文测试机的默认驱动集合里同时存在 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 Error 和 VUID-* 数量均为 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 --update、wsl --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_FILES、VK_LAYER_PATH 等用户可控变量。
旧资料常使用 VK_ICD_FILENAMES。它目前仍兼容,但 Khronos loader 文档已经将其标记为 deprecated,新脚本应改用 VK_DRIVER_FILES。
窗口能开,但画面是黑的
这通常已经不是 WSL 问题。按下面顺序查:
- Validation Layer 是否有 Error;
- Shader Module 是否来自正确的 SPIR-V;
- Pipeline 的颜色格式是否与 Swapchain 一致;
- Viewport/Scissor 是否覆盖窗口;
- Image Layout 是否转换正确;
- Command Buffer 是否真的提交;
- Present 是否等待 render-finished semaphore;
- 窗口缩放后是否重建 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、建立验证和构建流程,它足够好,而且两条执行路径反而提供了一个难得的对照实验。
参考资料
- Khronos Vulkan Tutorial:当前教程以 Vulkan 1.4、C++20、Dynamic Rendering、Timeline Semaphore 和 Slang 为主线。
- Khronos Vulkan Guide:版本、扩展、Validation、同步和常见陷阱的入口。
- Khronos Vulkan Samples:从基础渲染到扩展与性能实践的官方样例。
- Vulkan Loader Driver Interface:ICD 搜索路径以及
VK_DRIVER_FILES的准确语义。 - Vulkan Validation Overview:Validation Layer、VUID 与 Valid Usage。
- Vulkan Synchronization Examples:常见 barrier 与 queue 同步模式。
- Microsoft:在 WSL 中运行 Linux GUI 应用:WSLg、vGPU 与 GUI 前提。
- 本文完整示例源码:设备探测、Shader 构建与校验、交换链三角形、Validation 和 GitHub Actions。
- Microsoft:WSL 常见问题:
/dev/dxg和 WSL GPU 路径说明。 - Microsoft WSLg:WSLg 架构、Mesa D3D12 图形加速与问题跟踪。
- LunarG Vulkan SDK Packages:Ubuntu package 停止持续更新的说明与 SDK 下载入口。
微信公众号
欢迎关注「字与码」
如果这篇文章对你有用,也欢迎在微信里继续关注后续更新。
X / Twitter
关注 @ax2_zicode
更即时的技术观察、新文章提醒和一些短想法会发在 X 上。