0%

【Linux技术分享】Wine DXVA2 零拷贝(dmabuf)渲染流程

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Windows 端(PE 侧)                                      Linux 端(Unix 侧)
────────────────────────────────────── ──────────────────────────────────────
DirectXVideoDecoderGeneric_CreateVideoDecoder
│ zerocopy_enabled() ? 默认 = 1
│ 分配 exports[numSurfaces]
│ for each surface i:
│ 构造 vaapi_decoder_export_surface_params
│ DECODER_CALL(backend, decoder_export_surface)
│ │
│ └────────────── WINE_UNIX_CALL ─────────────▶ unix_vaapi_decoder_export_surface
│ └─ IWineVideoDecoder_ExportSurface
│ └─ WineVideoDecoderH264_ExportSurface
│ └─ pvaExportSurfaceHandle()
│ (libva) 导出 dmabuf fd
│ ◀────────── 返回 fd/width/height/pitch/uvOffset/uvPitch/size ──
│
│ exp->fd = fd; exp->imported = FALSE
│ wine_d3d9_surface_import_dmabuf(surface, fd, size, pitch)
│ └─ wined3d_texture_import_dmabuf()
│ └─ wined3d_cs_emit_import_dmabuf() ── 命令流 ──▶ wined3d_cs_exec_import_dmabuf()
│ ├─ EXT_memory_object_fd:
│ │ glCreateMemoryObjectsEXT
│ │ glImportMemoryFdEXT
│ │ glTexStorageMem2DEXT
│ └─ 或 EGL: unix_egl_dmabuf_import
│ └────────▶ Unix 侧建 EGLImage
│ exp->imported = TRUE ← 导入成功,该 surface 后续零拷贝
│
渲染时(每帧):
DirectXVideoDecoderGeneric_EndFrame
│ exports[surface].imported ?
│ ├─ TRUE → return S_OK (零拷贝:渲染端直接采样 dmabuf)
│ └─ FALSE → lock_image + LockRect + converter(CPU 拷贝回退)

图中虚线上半段是「导出」,发生在创建解码器时一次性完成;下半段是「导入」,也在创建解码器时完成;真正决定是否省拷贝,是每帧 EndFrame 时的判断。


三、代码参数说明

3.1 Linux 端:libva 导出 dmabuf

vaExportSurfaceHandle(libva API)

Wine 在 load_libva() 里用 dlsym 动态解析该符号(vaapi.c L148-L150),因为它属于「可选符号」——旧版 libva 没有它时零拷贝自动禁用、回退 CPU 拷贝,而不是让整个硬解失败。

1
2
3
4
5
6
7
VAStatus vaExportSurfaceHandle(
VADisplay dpy, // VA 显示句柄
VASurfaceID surface_id, // 要导出的解码 surface
uint32_t mem_type, // VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2
uint32_t flags, // VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS
void *descriptor // 输出:VADRMPRIMESurfaceDescriptor *
);

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
2
3
4
static HRESULT WINAPI WineVideoDecoderH264_ExportSurface(
IWineVideoDecoder *iface, UINT surfaceIndex,
int *fd, UINT *width, UINT *height, UINT *pitch,
UINT *uvOffset, UINT *uvPitch, UINT *size);
参数 方向 含义
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(触发回退):

  1. !pvaExportSurfaceHandle → E_NOTIMPL(libva 不支持);
  2. 布局必须为:num_objects >= 1、num_layers == 2、每层 1 个平面、格式 R8/GR88;
  3. 所有 object 必须是 linear(modifier == 0)且是 objects[0] 的同一块 buffer 的 dup(same_dmabuf);
  4. 紧密排列约束(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
2
3
4
5
6
7
8
9
static NTSTATUS unix_vaapi_decoder_export_surface(void *args)
{
struct vaapi_decoder_export_surface_params *params = args;
params->result = IWineVideoDecoder_ExportSurface(
(IWineVideoDecoder *)(UINT_PTR)params->decoder,
params->surfaceIndex, &params->fd, &params->width, &params->height,
&params->pitch, &params->uvOffset, &params->uvPitch, &params->size);
return S_OK;
}

跨进程参数结构(dxva2_private.h L152-L164):

1
2
3
4
5
6
7
8
9
10
11
12
13
struct vaapi_decoder_export_surface_params
{
UINT64 decoder; // 不透明的 IWineVideoDecoder 句柄
UINT surfaceIndex;
int fd; // OUT: dmabuf fd
UINT width; // OUT: 亮度宽
UINT height; // OUT: 亮度高
UINT pitch; // OUT: 亮度行 pitch
UINT uvOffset; // OUT: 色度平面偏移
UINT uvPitch; // OUT: 色度行 pitch
UINT size; // OUT: dmabuf 对象大小
HRESULT result;
};

注释说明:该结构所有字段都是固定宽度,32/64 位布局一致,无需 PTR32 转换(wow64 下直接透传)。


3.2 Windows 端:D3D9/wined3d 导入 dmabuf

① 解码器创建时的导出循环(genericdecoder.c L514-L560)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (zerocopy_enabled())
genericdecoder->exports = HeapAlloc(..., sizeof(*exports) * numSurfaces);

for (i = 0; genericdecoder->exports && i < numSurfaces; i++)
{
struct vaapi_surface_export *exp = &genericdecoder->exports[i];
struct vaapi_decoder_export_surface_params params;
...
DECODER_CALL(backend, decoder_export_surface, params); // → 跨到 Linux 端
if (FAILED(params.result)) { WARN(...); continue; } // 导出失败 → 该 surface 回退

exp->fd = params.fd; exp->width = ...; exp->size = params.size; ...

import_hr = wine_d3d9_surface_import_dmabuf( // → 导入到 D3D9
genericdecoder->surfaces[i], exp->fd, exp->size, exp->pitch);
if (FAILED(import_hr)) { exp->fd = -1; continue; } // 导入失败 → 回退
exp->imported = TRUE;
}

zerocopy_enabled()(L102-L115)读环境变量 WINE_DXVA2_ZEROCOPY,不设置时默认返回 1(开启),设 =0 可强制关闭走 CPU 拷贝。

每个 surface 的导出结果缓存到(dxva2_private.h L167-L179):

1
2
3
4
5
6
struct vaapi_surface_export
{
int fd;
UINT width, height, pitch, uvOffset, uvPitch, size;
BOOL imported; // TRUE 表示该 surface 已零拷贝,后续 EndFrame 跳过拷贝
};

② wine_d3d9_surface_import_dmabuf(d3d9/surface.c L468-L484)

Wine 私有的 D3D9 导出入口(见 d3d9.spec),供 dxva2 调用:

1
2
HRESULT WINAPI wine_d3d9_surface_import_dmabuf(
IDirect3DSurface9 *iface, int fd, unsigned int size, unsigned int pitch);
参数 含义
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
2
HRESULT CDECL wined3d_texture_import_dmabuf(
struct wined3d_texture *texture, int fd, unsigned int size, unsigned int pitch);

校验条件(不满足返回 WINED3DERR_INVALIDCALL):

  • texture->texture_ops == &texture_gl_ops(必须是 GL 后端)
  • level_count == 1 && layer_count == 1(单 level、单 layer)
  • format->id == WINED3DFMT_NV12
  • pitch >= 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
2
3
4
5
6
7
8
9
struct wined3d_cs_import_dmabuf
{
enum wined3d_cs_op opcode;
struct wined3d_texture *texture;
int fd;
unsigned int size;
unsigned int pitch;
HRESULT *hr;
};

命令流线程真正执行时,两条导入路径二选一:

路径 A:GL_EXT_memory_object_fd

1
2
3
glCreateMemoryObjectsEXT(1, &memory);
glImportMemoryFdEXT(memory, op->size, GL_HANDLE_TYPE_OPAQUE_FD_EXT, op->fd); // 接管 fd 所有权
glTexStorageMem2DEXT(target, 1, internal, row_pitch, height, memory, 0);

路径 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
2
3
4
5
6
7
8
9
10
11
12
if (last_surface >= 0 && This->exports && This->exports[last_surface].imported)
{
/* 渲染目标直接采样解码器的 dmabuf,帧数据已经在渲染端了 */
return S_OK; // 零拷贝,跳过拷贝
}

/* 否则走传统 CPU 拷贝 */
decoder_lock_image(...); // Linux 端 vaSyncSurface + vaGetImage + vaMapBuffer
IDirect3DSurface9_LockRect(...); // 锁定 D3D9 surface
converter->func(&imageInfo, &d3ddesc, &d3drect); // NV12→NV12 逐行 memcpy
IDirect3DSurface9_UnlockRect(...);
decoder_unlock_image(...);

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 强制关闭(调试用)。