Wine DXVA2 零拷贝(dmabuf)渲染流程
本文说明 Wine 下 DXVA2 硬件解码的零拷贝(zero-copy)路径是如何工作的:解码器(Linux 侧 VA-API)导出的显存如何被渲染端(Windows 侧 D3D9)直接采样,从而省掉一次 GPU→CPU→GPU 的数据搬运。文章分三个部分:先总述整体思路,再给出 Linux/Windows 两端的交互图与关键代码的参数说明,最后总结回退机制与限制条件。
一、总述
1. 为什么需要零拷贝
Wine 的 DXVA2 走 VA-API 硬解时,解码后的 NV12 数据放在 VA surface(GPU 显存)里。传统流程要把这帧数据交给 D3D9 渲染,需要两次拷贝:
1 | GPU(VA surface) --vaGetImage/vaMapBuffer--> CPU 内存 --LockRect/memcpy--> GPU(D3D9 surface) |
这对 1080p 影响尚可,但到了 4K(3840×2160 NV12,单帧约 12MB)就非常昂贵。零拷贝的思路是:让 D3D9 的渲染纹理直接”别名”解码器那块显存,渲染时直接采样,不再搬运数据。
2. 核心机制:dmabuf
Linux 上显卡驱动的显存对象可以用一个 dma-buf 文件描述符(fd) 在进程/子系统之间共享:
- VA-API 侧(Linux 端):
vaExportSurfaceHandle()把解码好的 VA surface 导出成 dmabuf fd(DRM PRIME 2)。 - D3D9/wined3d 侧(Windows 端):把这个 fd 导入成一个 GL 纹理(
glImportMemoryFdEXT或EGLImage),让纹理的底层存储直接指向这块显存。
两端通过 Wine 的 PE/Unix 跨进程调用(WINE_UNIX_CALL)衔接起来。
3. 一个关键前提:失败自动回退
零拷贝只在满足特定条件时生效,任何一个环节失败都会自动回退到传统的 CPU 拷贝路径,不会导致解码出错:
- 只针对 单 level、单 layer 的 NV12 纹理(DXVA2 解码渲染目标);
- 只支持 linear、非 padded、紧密排列 的 NV12 dmabuf 布局;
- 需要 GPU 驱动/系统支持 dmabuf 导入(
GL_EXT_memory_object_fd或EGL_EXT_image_dma_buf_import)。
回退是逐 surface 的:某个 surface 导出/导入失败,只有它自己走 CPU 拷贝,其它 surface 不受影响。
二、交互图:Linux 端 与 Windows 端
Wine 的 dxva2 模块物理上分成两半:
- Windows 端(PE 侧):运行在 Windows ABI 下,文件如
genericdecoder.c、d3d9/surface.c、wined3d/*.c。 - Linux 端(Unix 侧):真正调用 libva/GL,文件如
vaapi.c、vaapi-h264.c。
两者通过 WINE_UNIX_CALL 跨进程通信(wow64 下还有一层 PTR32 参数转换)。
1 | Windows 端(PE 侧) Linux 端(Unix 侧) |
图中虚线上半段是「导出」,发生在创建解码器时一次性完成;下半段是「导入」,也在创建解码器时完成;真正决定是否省拷贝,是每帧 EndFrame 时的判断。
三、代码参数说明
3.1 Linux 端:libva 导出 dmabuf
vaExportSurfaceHandle(libva API)
Wine 在 load_libva() 里用 dlsym 动态解析该符号(vaapi.c L148-L150),因为它属于「可选符号」——旧版 libva 没有它时零拷贝自动禁用、回退 CPU 拷贝,而不是让整个硬解失败。
1 | VAStatus vaExportSurfaceHandle( |
VADRMPRIMESurfaceDescriptor 关键字段(结合代码用法):
| 字段 | 含义 |
|---|---|
num_objects |
dmabuf 对象数量(Mesa 18.3 导出 2 个 dup,新 Mesa 共享 1 个) |
num_layers |
layer 数量,NV12 应为 2 |
objects[i].fd |
第 i 个 dmabuf 的文件描述符 |
objects[i].size |
对象字节大小 |
objects[i].drm_format_modifier |
modifier,0 = DRM_FORMAT_MOD_LINEAR(线性) |
layers[i].drm_format |
该 layer 的 DRM 格式,NV12 应为 R8(亮度) / GR88(色度) |
layers[i].num_planes |
该 layer 平面数,应为 1 |
layers[i].object_index[0] |
该 layer 数据所在 object 下标 |
layers[i].offset[0] |
该 layer 在 buffer 内的字节偏移 |
layers[i].pitch[0] |
该 layer 的行 pitch(字节) |
WineVideoDecoderH264_ExportSurface(vaapi-h264.c L835-L967)
这是 Linux 端真正被跨进程调用的导出实现,函数签名:
1 | static HRESULT WINAPI WineVideoDecoderH264_ExportSurface( |
| 参数 | 方向 | 含义 |
|---|---|---|
surfaceIndex |
in | 要导出的 surface 下标,需 < surfaceCount,否则 E_INVALIDARG |
fd |
out | 导出的 dmabuf fd(objects[0].fd),所有权仍归解码器 |
width / height |
out | 亮度平面宽高(像素) |
pitch |
out | 亮度行 pitch(字节),layers[0].pitch[0] |
uvOffset |
out | 色度平面在 buffer 内的字节偏移,layers[1].offset[0] |
uvPitch |
out | 色度行 pitch,layers[1].pitch[0] |
size |
out | dmabuf 对象大小(字节),会用 lseek(fd, SEEK_END) 校正 |
函数内部做了严格的布局校验,不满足就返回 E_FAIL(触发回退):
!pvaExportSurfaceHandle→E_NOTIMPL(libva 不支持);- 布局必须为:
num_objects >= 1、num_layers == 2、每层 1 个平面、格式R8/GR88; - 所有 object 必须是 linear(modifier == 0)且是
objects[0]的同一块 buffer 的 dup(same_dmabuf); - 紧密排列约束(L922-L925):
layers[0].pitch[0] >= width(行可以有 padding,但不能比宽窄)layers[1].offset[0] == layers[0].pitch[0] * height(色度紧跟亮度)layers[1].pitch[0] == layers[0].pitch[0](亮度/色度同 pitch)size >= pitch * height * 3/2
Unix 侧入口 unix_vaapi_decoder_export_surface(vaapi.c L981-L988)
它是 PE 侧跨进程调用落地的函数,把 vaapi_decoder_export_surface_params 拆开转发给虚表方法:
1 | static NTSTATUS unix_vaapi_decoder_export_surface(void *args) |
跨进程参数结构(dxva2_private.h L152-L164):
1 | struct vaapi_decoder_export_surface_params |
注释说明:该结构所有字段都是固定宽度,32/64 位布局一致,无需 PTR32 转换(wow64 下直接透传)。
3.2 Windows 端:D3D9/wined3d 导入 dmabuf
① 解码器创建时的导出循环(genericdecoder.c L514-L560)
1 | if (zerocopy_enabled()) |
zerocopy_enabled()(L102-L115)读环境变量 WINE_DXVA2_ZEROCOPY,不设置时默认返回 1(开启),设 =0 可强制关闭走 CPU 拷贝。
每个 surface 的导出结果缓存到(dxva2_private.h L167-L179):
1 | struct vaapi_surface_export |
② wine_d3d9_surface_import_dmabuf(d3d9/surface.c L468-L484)
Wine 私有的 D3D9 导出入口(见 d3d9.spec),供 dxva2 调用:
1 | HRESULT WINAPI wine_d3d9_surface_import_dmabuf( |
| 参数 | 含义 |
|---|---|
iface |
目标 D3D9 surface,必须是 mip 0(sub_resource_idx == 0),否则 D3DERR_INVALIDCALL |
fd |
要导入的 dmabuf fd |
size |
dmabuf 字节大小 |
pitch |
dmabuf 行 pitch(可能比 surface 宽,带 padding) |
实现里加锁后转发给 wined3d_texture_import_dmabuf(surface->wined3d_texture, fd, size, pitch)。
③ wined3d_texture_import_dmabuf(texture.c L2172-L2199)
1 | HRESULT CDECL wined3d_texture_import_dmabuf( |
校验条件(不满足返回 WINED3DERR_INVALIDCALL):
texture->texture_ops == &texture_gl_ops(必须是 GL 后端)level_count == 1 && layer_count == 1(单 level、单 layer)format->id == WINED3DFMT_NV12pitch >= resource->width(行可 padding,但不能比纹理窄)
校验通过后交给命令流线程:wined3d_cs_emit_import_dmabuf(...)。
④ 命令流:wined3d_cs_emit_import_dmabuf / wined3d_cs_exec_import_dmabuf(cs.c L3106-L3128 / L2987-L3104)
提交侧构造命令:
1 | struct wined3d_cs_import_dmabuf |
命令流线程真正执行时,两条导入路径二选一:
路径 A:GL_EXT_memory_object_fd
1 | glCreateMemoryObjectsEXT(1, &memory); |
路径 B:EGL_EXT_image_dma_buf_import
通过 WINE_UNIX_CALL(unix_egl_dmabuf_import, ...) 在 Linux 侧创建 EGLImage 并绑定到当前纹理;此路径 eglCreateImageKHR 不接管 fd,fd 仍归解码器、每次导入复用。
两者都满足「整块 NV12 buffer 别名成一个单平面 R8 纹理、行宽为 row_pitch」。成功后设置 texture_rgb.row_pitch、置 WINED3D_TEXTURE_RGB_ALLOCATED 标志,完成零拷贝导入。
3.3 Windows 端:渲染时决定是否拷贝(genericdecoder.c L345-L384)
每帧 DirectXVideoDecoderGeneric_EndFrame 的判断:
1 | if (last_surface >= 0 && This->exports && This->exports[last_surface].imported) |
converter 是 convert_nv12_nv12(genericdecoder.c L46-L73),逐行把 imageInfo.buffer 拷贝到 d3drect.pBits,这就是零拷贝要绕开的那条路径。
四、总结
1. 整条链路一句话
解码器创建时,Linux 端用 vaExportSurfaceHandle 把每个 VA surface 导出成 dmabuf fd,Windows 端再用 wined3d_texture_import_dmabuf 把这个 fd 导入成 D3D9 纹理的底层存储;渲染时若该 surface 导入成功(imported == TRUE)就直接采样,否则回退 CPU 拷贝。
2. 两端职责
| 端 | 文件 | 职责 |
|---|---|---|
| Linux | vaapi.c / vaapi-h264.c |
动态加载并调用 libva 的 vaExportSurfaceHandle,校验 NV12 布局,产出 fd + 几何信息 |
| Windows | genericdecoder.c |
编排导出/导入,缓存 exports[],渲染时决定是否拷贝 |
| Windows | d3d9/surface.c、wined3d/texture.c、wined3d/cs.c |
把 fd 导入成 GL 纹理,让纹理别名解码器显存 |
3. 回退机制(健壮性)
零拷贝是「锦上添花」,任何一步失败都不会影响正确性,只会退回到 CPU 拷贝:
- libva 无
vaExportSurfaceHandle符号 →E_NOTIMPL回退; - 布局不是 linear/紧密排列 NV12 →
E_FAIL回退; - 系统不支持 dmabuf 导入(缺
EXT_memory_object_fd且缺 EGL dma_buf)→WINED3DERR_NOTAVAILABLE回退; - 单个 surface 导出/导入失败 → 仅该 surface 回退(
imported保持 FALSE)。
4. 关键限制
- 仅支持 GL 后端 + 单 level + 单 layer + NV12 纹理;
- 仅支持 linear、非 padded、紧密排列的 NV12 dmabuf;
- 默认开启,可用
WINE_DXVA2_ZEROCOPY=0强制关闭(调试用)。