探究Linux图形系统(2025年)

Lobsters Hottest 新闻

摘要

深入探究Linux图形栈,从GPU三角形绘制出发,经过Mesa3D、GLFW、OpenGL、Vulkan、Wayland和Linux DRM,理解整个系统的工作原理。

<p><a href="https://lobste.rs/s/wenqxh/investigating_linux_graphics_2025">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/30 07:35

# 探究 Linux 图形系统 – Thomas Leonard 的博客 来源: https://roscidus.com/blog/blog/2025/06/24/graphics/ 我学习了如何用 GPU 绘制三角形,然后追踪代码,了解图形系统如何运作(或不运作),涉及 Mesa3D、GLFW、OpenGL、Vulkan、Wayland 和 Linux DRM。 **目录** - 引言 (https://roscidus.com/blog/blog/2025/06/24/graphics/#introduction) - 总览 (https://roscidus.com/blog/blog/2025/06/24/graphics/#overview) - OpenGL (https://roscidus.com/blog/blog/2025/06/24/graphics/#opengl) - Vulkan (https://roscidus.com/blog/blog/2025/06/24/graphics/#vulkan) - 同步 (https://roscidus.com/blog/blog/2025/06/24/graphics/#synchronisation) - 初次追踪尝试 (https://roscidus.com/blog/blog/2025/06/24/graphics/#first-attempt-at-tracing) - 移除 GLFW (https://roscidus.com/blog/blog/2025/06/24/graphics/#removing-glfw) - 移除 Vulkan 的 Wayland 扩展 (https://roscidus.com/blog/blog/2025/06/24/graphics/#removing-vulkans-wayland-extension) - Wayland 详述 (https://roscidus.com/blog/blog/2025/06/24/graphics/#wayland-walk-through) - 使用 bpftrace 查看内核细节 (https://roscidus.com/blog/blog/2025/06/24/graphics/#kernel-details-with-bpftrace) - 启动与库加载 (https://roscidus.com/blog/blog/2025/06/24/graphics/#start-up-and-library-loading) - 枚举设备 (https://roscidus.com/blog/blog/2025/06/24/graphics/#enumerating-devices) - 设置管线 (https://roscidus.com/blog/blog/2025/06/24/graphics/#setting-up-the-pipeline) - 渲染一帧 (https://roscidus.com/blog/blog/2025/06/24/graphics/#rendering-one-frame) - 重新审视错误 (https://roscidus.com/blog/blog/2025/06/24/graphics/#re-examining-the-errors) - 结论 (https://roscidus.com/blog/blog/2025/06/24/graphics/#conclusions) ## 引言 过去,为了避开 Linux 上的图形驱动问题,我一直只使用 Intel 集成显卡。但由于选错了主板,最终不得不配一块独立显卡。现在我的电脑从挂起恢复需要 14 秒,而且 `dmesg` 不断输出这类信息: `` [59829.886009] [drm] Fence fallback timer expired on ring sdma0 [59830.390003] [drm] Fence fallback timer expired on ring sdma0 [59830.894002] [drm] Fence fallback timer expired on ring sdma0 [79622.739495] amdgpu 0000:01:00.0: [drm:amdgpu_ring_test_helper [amdgpu]] *ERROR* ring comp_1.0.1 test failed (-110) [79622.909019] amdgpu 0000:01:00.0: [drm:amdgpu_ring_test_helper [amdgpu]] *ERROR* ring comp_1.0.2 test failed (-110) [79623.075056] amdgpu 0000:01:00.0: [drm:amdgpu_ring_test_helper [amdgpu]] *ERROR* ring comp_1.0.3 test failed (-110) [79623.241971] amdgpu 0000:01:00.0: [drm:amdgpu_ring_test_helper [amdgpu]] *ERROR* ring comp_1.0.4 test failed (-110) [79623.408604] amdgpu 0000:01:00.0: [drm:amdgpu_ring_test_helper [amdgpu]] *ERROR* ring comp_1.0.6 test failed (-110) [80202.893020] [drm] scheduler comp_1.0.1 is not ready, skipping [80202.893023] [drm] scheduler comp_1.0.2 is not ready, skipping [80202.893024] [drm] scheduler comp_1.0.3 is not ready, skipping [80202.893025] [drm] scheduler comp_1.0.4 is not ready, skipping [80202.893025] [drm] scheduler comp_1.0.6 is not ready, skipping [80202.936910] [drm] scheduler comp_1.0.1 is not ready, skipping `` 但什么是“fence”或“sdma0”环?这些 `comp_` 调度器是什么?为什么当它们不够就绪时 Linux 会崩溃 (https://gitlab.freedesktop.org/drm/amd/-/issues/3579)?以及为什么升级 NixOS 后 Firefox 在播放视频时会挂起 (https://github.com/NixOS/nixpkgs/issues/409093)?我觉得是时候了解 Linux 图形系统本应如何工作了…… ## 总览 要在屏幕上显示内容,我们需要分配一块内存(称为*帧缓冲*),用于保存每个像素的颜色。计算完所有(数百万个)颜色值后,我们将帧缓冲的地址告知*显示硬件*,硬件再将所有值发送到显示器显示。在硬件执行此操作时,我们可以渲染下一帧到另一个帧缓冲。计算机并不擅长这种工作,但*显卡*能加速。显卡就像另一台计算机,拥有自己的内存、处理器和显示硬件,但针对图形进行了优化: 主计算机(主机) 通常拥有*少量*但*非常快*的处理器。 显卡 拥有*大量*但*相对较慢*的处理器。 显卡架构之所以有用,是因为我们可以将屏幕分割成许多小图块,并在不同处理器上并行渲染。使处理器相对较慢运行可以节省能量(和热量),从而允许我们使用更多处理器。 注意:GPU(图形处理单元)不一定要在独立显卡上;它也可以是主计算机的一部分,并使用主内存而非专用 RAM。 通常我们运行多个应用程序,并让它们共享屏幕。理想情况下,每个应用程序(如 Firefox)在 GPU 上运行代码,将其窗口内容渲染到 GPU 内存(1),然后将该内存的引用共享给*显示服务器*(2)。显示服务器(在我的例子中是 Sway)在 GPU 上运行更多代码(3),将该窗口复制到最终图像(4),然后硬件将图像发送到屏幕(5): 使用显卡的 Wayland 桌面 (https://roscidus.com/blog/images/graphics/arch.svg) 应用程序进程通过 Linux 内核驱动(我这里是 `amdgpu`)向 GPU 发送指令。每个 GPU 都有自己的 API,且这些 API 非常底层,因此应用程序通常使用 Mesa (https://mesa3d.org/) 库来提供跨所有设备的统一 API。Mesa 为所有不同 GPU 实现了后端,如果没有可用的 GPU,也会提供软件渲染作为备选方案。Linux 图形栈概览 (https://lwn.net/Articles/955376/) 有更多解释,但我想亲自尝试一下…… ## OpenGL Mesa 支持 OpenGL,这是一个跨平台的标准图形 API。然而,还需要一些平台特定代码来打开窗口并连接合适的后端。经过一番搜索,我找到了 GLFW (https://www.glfw.org/)(图形库框架),它有一个很好的教程 (https://www.glfw.org/docs/latest/quick.html),演示如何在窗口中绘制三角形。它成功了,我得到了一个窗口,里面有一个色彩缤纷的旋转三角形,即使全屏也能平滑动画,且 CPU 负载为零(使用 `LIBGL_ALWAYS_SOFTWARE=true` 可与软件渲染对比): OpenGL 三角形 (https://roscidus.com/blog/images/graphics/opengl.png) 首先进行初始化: 1. 创建窗口 (`window = glfwCreateWindow(640, 480, ...)`) 2. 将其设为 OpenGL 目标 (`glfwMakeContextCurrent(window)`) 3. 创建三角形顶点缓冲区并发送到显卡。 4. 为显卡编译顶点和片段“着色器”。在 GPU 上运行的程序称为“着色器”(无论是否进行着色处理)。它们用类似 C 的语言编写,并作为字符串嵌入到示例的 C 源码中。 顶点着色器为三角形的 3 个顶点各运行一次,根据输入参数旋转它们。片段着色器随后为旋转三角形覆盖的每个屏幕像素运行,选择其颜色。这基本上就是恒等函数,因为 OpenGL 会自动插值三个顶点的颜色,并将其作为输入传递给片段着色器。 然后示例运行主循环,每帧渲染: 1. 将 OpenGL 视口设置为窗口大小。 2. 清除为背景颜色。 3. 设置顶点着色器输入为所需旋转角度。 4. 使用着色器绘制三角形。 5. 使最终图像被显示(`glfwSwapBuffers(window)`)。 Mesa 还提供其他几种 API 作为 OpenGL 的替代。OpenGL ES(嵌入式系统 OpenGL)基本上是 OpenGL 的子集,但也有一些自己的小改进。但我对 Vulkan 更感兴趣…… ## Vulkan Vulkan 自称是“一个低级 API,移除了前几代图形 API 中的许多抽象层”。例如,每个 OpenGL 驱动都包含一个着色器语言编译器,而在 Vulkan 中,你需要使用外部工具将着色器源码编译为 SPIR-V 字节码 (https://en.wikipedia.org/wiki/Standard_Portable_Intermediate_Representation),然后将字节码直接传递给驱动。因此,Vulkan 驱动应该更简单、更容易理解。 Vulkan 教程 (https://docs.vulkan.org/tutorial/latest/00_Introduction.html) 警告说:“这些好处的代价是你必须使用一个冗长得多的 API”。确实;OpenGL 三角形示例只有 171 行 C 代码,而 Vulkan 的三角形示例有 900 行 C++!但这正是我想要的;详细分解各个步骤。需要创建很多对象,我最终画了一张图来理清关系: Vulkan 对象依赖关系 (https://roscidus.com/blog/images/graphics/vulkan-deps.svg) 以下是设置步骤: - 和之前一样,先创建一个普通的 Wayland 窗口。 - 创建一个 Vulkan*实例*,请求 Wayland 支持扩展。 - 使用实例和窗口创建一个 Vulkan*表面*。 - 使用实例获取*物理设备*列表。在我的例子中,找到了两个设备:`AMD Radeon RX 550 Series` 和 `llvmpipe`(软件渲染)。奇怪的是,我的 Intel 集成 GPU 没有出现。[更新:在 BIOS 设置中,“Internal Graphics”设置为“Auto”,这禁用了它] - 为要使用的物理设备创建一个*设备*。每个设备可以有多个“族”的队列,我们需要指定每种类型需要多少队列。我们需要选择一个支持可以 a) 渲染图形,以及 b) 将渲染图像呈现到我们的 Wayland 表面的队列的设备。我的 AMD GPU 支持三个队列族,其中两个可以呈现到表面,一个可以渲染图形。我选择使用两个队列,不过在我的显卡上也可以只用一个队列来完成两项任务。 - 创建一个*渲染通道*,包含关于渲染将做什么的元数据。在教程示例中,这指定了渲染将清除帧缓冲,并且它依赖于获取一个可绘制的图像。 - 通过加载预编译的 SPIR-V 字节码创建*着色器*。 - 创建一个*图形管线*,使用着色器实现渲染通道。 - 为设备和表面创建一个*交换链*。交换链创建多个*图像*,可以渲染到这些图像上并将其显示。 - 将交换链中的每个图像包装在一个*图像视图*中,图像视图指定数据的格式。 - 将每个图像视图用*帧缓冲*包装,用于*渲染通道*。 - 创建一个*命令池*来管理命令缓冲区。 - 从池中获取一个*命令缓冲区*。 现在准备好主循环: - 从*交换链*获取下一个*图像*(请求 Vulkan 在图像可被覆盖时通知一个*信号量*)。 - 将我们想要执行的操作记录到*命令缓冲区*中: 1. 将图像的*帧缓冲*清除为背景颜色。 2. 将管线输入的视口设置为当前窗口大小。 3. 使用*图形管线*渲染场景(本例中只有一个三角形)。 - 将*命令缓冲区*加入设备*图形队列*。我们传递要等待的信号量(根据*渲染通道*的要求),另一个要完成的信号量,以及一个*围栏*来通知主机命令缓冲区可被重用。“围栏”实际上是与主机共享的信号量(而 Vulkan 用“信号量”表示用于 GPU 内部信号的一种)。 - 将*图像*的呈现加入设备*呈现队列*,要求它在渲染信号量被通知时呈现图像(使用显示服务器)。 嗯,工作量真大!而且让三角形动画还需要在此基础上做更多工作。但我认为我对工作原理有了比 OpenGL 更好的理解,并且我怀疑这将更容易追踪。 `vulkaninfo` 命令会输出检测到的 GPU 的详细信息。我的一些重点: `` Instance Extensions: count = 24 VK_KHR_wayland_surface : extension revision 6 VK_KHR_xcb_surface : extension revision 6 VK_KHR_xlib_surface : extension revision 6 GPU id : 0 (AMD Radeon RX 550 Series (RADV POLARIS11)) [VK_KHR_wayland_surface]: VkSurfaceCapabilitiesKHR: minImageCount = 4 VkQueueFamilyProperties: queueProperties[0]: queueFlags = QUEUE_GRAPHICS_BIT | QUEUE_COMPUTE_BIT | QUEUE_TRANSFER_BIT queueProperties[1]: queueFlags = QUEUE_COMPUTE_BIT | QUEUE_TRANSFER_BIT queueProperties[2]: queueFlags = QUEUE_SPARSE_BINDING_BIT GPU id : 1 (llvmpipe (LLVM 19.1.7, 256 bits)) [VK_KHR_wayland_surface]: `` Vulkan 的一个有趣特性是你可以启用“验证层”,这些层会在开发过程中检查你是否正确使用了 API,但在生产环境可以关闭以获得更好性能。例如,它显示教程中的同步并不完全正确(详情见 Swapchain Semaphore Reuse (https://docs.vulkan.org/guide/latest/swapchain_semaphore_reuse.html);教程只使用了一个 `renderFinishedSemaphore`,而不是每个帧缓冲一个)。 ## 同步 这里有趣的一点是,交换链将图像发送给合成器(显示服务器)时,并不等待渲染完成。这是如何实现的?有两种方法可以确保合成器在渲染完成之前不会尝试使用图像:*隐式*或*显式*同步。 对于隐式同步,Linux 内核会跟踪哪些 GPU 作业正在访问哪些缓冲区。因此,当我们提交一个作业来渲染三角形时,内核将作业的完成围栏附加到输出缓冲区。当合成器(我的例子中是 Sway)提交一个 GPU 作业来复制图像时,内核会先等待围栏。 这显然存在各种问题。例如,合成器可能更愿意使用上一个已经完成的帧,而不是等待这一帧。有一个显式同步的 Wayland 协议可以解决这个问题,但我的 Sway 版本不支持它。更多细节,请参阅 Bridging the synchronization gap on Linux (https://www.collabora.com/news-and-blog/blog/2022/06/09/bridging-the-synchronization-gap-on-linux/) 和 Explicit sync (https://zamundaaa.github.io/wayland/2024/04/05/explicit-sync.html)。 ## 初次追踪尝试 渲染的图像如何从测试应用程序传到 Wayland 合成器(显示服务器)?我用 `WAYLAND_DEBUG=1` 运行,记录教程应用程序发送和接收的所有消息: `` $ WAYLAND_DEBUG=1 ./vulkan-test {Default Queue} -> wl_display#1.get_registry(new id wl_registry#2) {Default Queue} -> wl_display#1.sync(new id wl_callback#3) {Display Queue} wl_display#1.delete_id(3) ... `` 但输出令人困惑,`strace` 显示测试应用程序连接了 4 次显示服务器: `` $ strace -e connect ./vulkan-test 2>&1 | grep /wayland connect(3, {sa_family=AF_UNIX, sun_path="/run/user/1000/wayland-1"}, 27) = 0 connect(5, {sa_family=AF_UNIX, sun_path="/run/user/1000/wayland-1"}, 27) = 0 connect(23, {sa_family=AF_UNIX, sun_path="/run/user/1000/wayland-1"}, 27) = 0 connect(23, {sa_family=AF_UNIX, sun_path="/run/user/1000/wayland-1"}, 27) = 0 `` 更复杂的是,示例使用了 6 个 Wayland 队列: `` $ WAYLAND_DEBUG=1 ./vulkan-test 2>&1 | sed -n 's/.*\({[^}]*}\).*/\1/p' | sort | uniq -c 618 {Default Queue} 135 {Display Queue} 288 {mesa formats query} 72 {mesa image count query} 144 {mesa present modes query} 385 {mesa vk display queue} `` 总共,测试应用程序向 Sway 询问支持的扩展(协议)列表 14 次!它反复绑定相同的扩展(并调用 `zwp_linux_dmabuf_v1.get_default_feedback`): `` $ WAYLAND_DEBUG=1 ./vulkan-test 2>&1 | grep

相似文章

Linux延迟测量与合成器调优

Lobsters Hottest

一项详细调查,使用基于Teensy的LDAT工具测量游戏中的Linux延迟,在KDE Wayland下的Nvidia GPU上使用各种设置测量点击到光子延迟,并与Windows进行比较。

我为 Emacs 构建了一个 GPU 后端

Hacker News Top

作者描述了如何在 macOS 上使用 Metal、在 Linux 上使用 OpenGL 为 Emacs 构建基于 GPU 的显示后端,从而提升渲染性能并启用视频播放和动画光标等新效果,且无需修改核心重新显示引擎。