dri3_alloc_render_buffer

mesa/src/loader/loader_dri3_helper.c::dri3_alloc_render_buffer(), 这个函数不长,但却涉及到了DRI框架的许多重要概念,buffer共享(DMABUF), GPU offload (PRIME), buffer同步(X client和server),送显(present), modifiers等等,所以非常值得深入分析。

dri3_alloc_render_buffer

1
2
3
4
static struct loader_dri3_buffer *
dri3_alloc_render_buffer(struct loader_dri3_drawable *draw,
unsigned int fourcc,
int width, int height, int depth);

dri3_alloc_render_buffer() 的任务是创建渲染 buffer (包括 front 或 back), 并向 X11 导出它们的 fd。驱动当然不是一下子把 buffers[] 数组填满, 它是"按需创建", buffers[] 像是提供这么多 buffer slot, 当 slot 后面是一个空闲的 buffer, 那就将它返回,否则才真正创建。

Drawable 和 Buffer

struct loader_dri3_drawablestruct loader_dri3_buffer 这两个结构体,一个作为 dri3_alloc_render_buffer() 的主要参数,一个作为它的返回值类型,可谓是了解 DRI3 扩展下的数据流的关键。

classDiagram
    direction RL
    loader_dri3_drawable o-- loader_dri3_buffer : LOADER_DRI3_NUM_BUFFERS
    class loader_dri3_drawable{
        +xcb_connection_t * conn
        +xcb_screen_t * screen
        +__DRIdrawable *dri_drawable
        +xcb_drawable_t drawable
        +xcb_window_t window
        +int width
        +int height
        +int depth
        +uint8_t have_back
        +uint8_t have_fake_front
        +uint64_t send_sbc
        +uint64_t recv_sbc
        +uint64_t ust
        +uint64_t msc
        +uint64_t notify_ust
        +uint64_t notify_msc
        +loader_dri3_buffer *buffers[5]
        +int cur_back
        +int cur_num_back
        +int max_num_back
        +int cur_blit_source
        +uint32_t *stamp
    }
    note for loader_dri3_buffer "bool busy;\nSet on swap\nCleared on IdleNotify"
    class loader_dri3_buffer{
        +__DRIimage * image
        +uint32_t pixmap
        +__DRIimage * linear_buffer
        +uint32_t fence
        +xshmfence * shm_fence
        +bool busy
        +bool own_pixmap
        +bool reallocate
        +uint32_t num_planes
        +uint32_t size
        +int strides[4]
        +int offsets[4]
        +uint64_t modifier
        +uint32_t cpp
        +uint32_t flags
        +uint32_t width
        +uint32_t height
        +uint64_t last_swap
    }

同步

  • 当我们谈论 X client 和 server 之间的 Buffer 同步时是在说什么?

在DRI3扩展下, render buffer (BO作为GPU 的render target) 是一开始由X client (例如一个 3D App)创建的(可能不止一个), render buffer 创建好后随即会通过 __DRIimageExtension 的 queryImage() 查询到该buffer 的 FD (drmPrimeHandleToFD, 后面会将该 FD 传送给 X server), 而在 X 的 compositor, 拿到 GPU 的渲染结果实际上就是通过该 FD (drmPrimeFDToHandle) 将 render buffer gbm_bo_import() 到 X server 进程, 并创建X 的 Pixmap (Pixmap 的Backing BO就是当初App进程创建的)后读取渲染结果进行合成。

该过程通过 X client 和 server 进程间的 buffer 共享实现了 render buffer 的零拷贝。
而同步问题也在这个过程中产生,当 render buffer 被 server 进程导入后用于合成时,渲染结果什么时候被读取完毕(render buffer IDLE 状态),需要告知client 进程(client不能在上一帧数据未读取完毕前同时再渲染到同一个render buffer)。同样client 也须在 server 读取当前帧之前告知server 渲染是否已经完成。

这样 X client 和 server 之间的buffer 同步问题就产生了。

以上3步实际上是利用4个字节(sizeof(struct xshmfence))大小的共享内存在X server 和 client 进程间通过原子操作和 futex 系统调用达到两个进程对 render buffer 的同步访问。

(以上4个字节共享内存的结论是基于futex和原子操作实现的 xshmfence 的版本)

由于client 创建的render buffer 是与 X server 共享的,所以这个 render buffer 被两个进程读写时须要同步,Mesa3D 中是使用 xshmfence 来完成这个需求的。xshmfence 顾名思义它是基于共享内存的,采用它实现进程间对 render buffer 操作的同步,好处就是只需要将 xshmfence 映射到一个 X server SyncFence, 通过一个简单的函数调用(xshmfence_await(struct xshmfence *f))就可将调用进程(client process)阻塞直到 X server 完成对 render buffer的操作再被操作系统唤醒,而无需通过接收网络事件(socket event)来确定X server 是否已经完成对 render buffer 的使用。

导入/导出

render buffer 的导入/导出操作是Linux 下Buffer 共享和同步的一个标准流程,不仅仅是在 DRM 子系统使用,在Linux的其它子系统也广泛使用,如Video4Linux, Networking。这里仅仅将 mesa 中的实现与DMABUF 机制中的角色对应一下,作为一个DMABUF的应用案例分析。

sequenceDiagram
    autonumber
    participant App
    participant Mesa
    participant X11

    App-->>Mesa: eglMakeCurrent()
    Mesa->>Mesa: dri_st_framebuffer_validate()
    Mesa->>Mesa: dri2_allocate_textures()
    Mesa->>Mesa: loader_dri3_get_buffers()
    note left of Mesa: Return all necessary buffers and allocating as needed
    Mesa->>Mesa: dri3_get_buffer()
    rect rgb(191, 223, 255)
    Mesa->>Mesa: dri3_alloc_render_buffer()
    Mesa-->>X11: xcb_dri3_get_supported_modifiers()
    X11-->>Mesa: xcb_dri3_get_supported_modifiers_reply()
    Mesa->>Mesa: loader_dri_create_image()
    Mesa->>Mesa: dri2_create_image()
    Mesa->>Mesa: xxx_resource_create()
    rect rgb(200, 150, 255)
    Mesa->>Mesa: dri2_query_image_by_resource_handle(__DRIimage)
    Mesa->>Mesa: xxx_resource_get_handle()
    Mesa->>Mesa: xxx_bo_export()
    note right of Mesa: Mesa 导出 FD
    opt xcb_dri3_pixmap_from_buffers(buffer_fds)
        Mesa-->>X11: xcb_send_requests_with_fds(buffer_fds)
    end
    end
    rect rgb(200, 150, 255)
    note left of X11: X11 导入 Buffer
    X11->>X11: proc_dri3_pixmap_from_buffers()
    X11->>X11: dri3_pixmap_from_fds()
    X11->>X11: glamor_pixmap_from_fds()
    alt GBM_BO_WITH_MODIFIERS
        X11->>X11: gbm_bo_import(type=GBM_BO_IMPORT_FD_MODIFIER)
    else
        X11->>X11: gbm_bo_import(type=GBM_BO_IMPORT_FD)
    end
    end
    end

所有情况都是 Mesa (DRI client) 创建导出 RenderBuffer,X11 导入吗?

  • eglCreatePbufferSurface()

实际上 PBuffer 是离屏渲染使用的,它是由 X11 创建 buffer, X11 导出 FD, Mesa (应用进程) 导入作为伪前缓冲 (fake front buffer) 使用。

通常 eglCreate***Surface() 需要 App 先调用 XCreateWindow() 让 X11 创建 Pixmap, 但是 eglCreatePbufferSurface() 不用,它由 Mesa 调用 xcb_create_pixmap() 让 X11 创建 Pixmap, 之后 Mesa 再调用 xcb_dri3_buffers_from_pixmap() 让 X11 导出 Pixmap(gbm_bo) 关联的 FD, 由 Mesa 导入。

sequenceDiagram
    autonumber
    participant App
    participant Mesa
    participant X11

    App-->>Mesa: eglCreatePbufferSurface()
    rect rgb(191, 223, 255)
    Mesa->>Mesa: dri3_create_surface(type=EGL_PBUFFER_BIT)
    Mesa-->>X11: xcb_generate_id()
    X11-->>Mesa: drawable ID (uint32_t)
    Mesa-->>X11: xcb_create_pixmap()
    App-->>Mesa: eglBindTexImage()
    Mesa->>Mesa: dri_st_framebuffer_validate()
    Mesa->>Mesa: dri2_allocate_textures()
    Mesa->>Mesa: loader_dri3_get_buffers()
    Mesa->>Mesa: dri3_get_pixmap_buffer()
    rect rgb(200, 150, 255)
    note left of X11: X11 导出 FD(调用 gbm_bo_get_fd(gbm_bo*))
    Mesa-->>X11: xcb_dri3_buffers_from_pixmap()
    X11-->>Mesa: xcb_dri3_buffers_from_pixmap_reply()
    Mesa-->>X11: loader_dri3_create_image_from_buffers()
    X11-->>Mesa: xcb_dri3_buffers_from_pixmap_reply_fds()
    end
    rect rgb(200, 150, 255)
    note left of Mesa: Mesa 导入 Buffer
    Mesa->>Mesa: dri2_from_dma_bufs2()
    Mesa->>Mesa: dri2_create_image_from_fd()
    Mesa->>Mesa: dri2_create_image_from_winsys()
    Mesa->>Mesa: xxx_resource_from_handle()
    Mesa->>Mesa: xxx_bo_import()
    end
    end
  • DRI2

DRI3 与 DRI2 的一个主要区别就是在 DRI3, RenderBuffer 是由 DRI client (Mesa) 创建后通过 bo_export() 导出其 DMA-BUF fd,由 X server 通过 proc_dri3_pixmap_from_buffers() 导入的。 而且 DRI2 不仅导入导出方面是反着的,而且 DRI client 导入的是 GEM name(不是 DMA-BUF fd), 据说 GEM name 这玩意虽然也可以在进程间传递,但相比 DMA-BUF fd 很不安全,所以新驱动都用 DMA-BUF fd 在进程间传递 Buffer, 只有旧驱动可能还有 GEM name。

实现上没太明白 GEM name 为什么不安全,大家都是 32-bit 整数,GEM name 能被猜,文件描述符 fd 也能被猜啊

找到解释了,PRIME Buffer Sharing - Overview and Lifetime Rules, 文件描述符 (file descriptor) 必须通过 UNIX domain sockets 在应用之间 send,不可能像全局唯一的 GEM names 一样被猜。 send over UNIX domain sockets 应该就是 XCB 库实现的这个函数 xcb_send_requests_with_fds()

dri3_find_back

1
2
3
4
5
6
7
/**
* Find an idle back buffer. If there isn't one, then
* wait for a present IdleNotify event from the X server
*/
static int
dri3_find_back(struct loader_dri3_drawable *draw,
bool prefer_a_different);

dri3_find_back() 的任务是查找 buffers[] 中空闲 buffer 的 slot (array index), 如果存在,返回其 index, 否则等待 (dri3_wait_for_event_locked())

如何知道 buffer 是否空闲呢? buffer->busy, 这个标志在 SwapBuffers 时会置为 true, mesa 收到 IdleNotify 事件后置为 false

sequenceDiagram
    autonumber
    participant Mesa
    participant X11

    Mesa->>Mesa: dri3_find_back
    note right of Mesa: 选取一个合适的空闲 slot id, buffers[id]有可能是空, 这时需要创建 buffer: dri3_alloc_render_buffer()
    rect rgb(200, 150, 255)
    note right of Mesa: mtx_lock()
    alt pefer_a_different==false
        Mesa-->>X11: dri3_flush_present_events()
        loop xcb_poll_for_special_event
            X11-->>Mesa: xcb_present_generic_event_t
        end
        Mesa->>Mesa: dri3_handle_present_event()
    else perfer_a_different==true
        Mesa-->>X11: xcb_flush()
        loop dri3_wait_for_event_locked()
            X11-->>Mesa: xcb_present_generic_event_t
        end
        Mesa->>Mesa: dri3_handle_present_event()
    end
    note right of Mesa: mtx_unlock()
    end

loader_dri3_swap_buffers_msc

1
2
3
4
5
6
7
8
9
/**
* Make the current back buffer visible using the present extension
*/
int64_t
loader_dri3_swap_buffers_msc(struct loader_dri3_drawable *draw,
int64_t target_msc, int64_t divisor, int64_t remainder,
unsigned flush_flags,
const int *rects, int n_rects,
bool force_copy);
flowchart TD
    A[eglSwapBuffers]
    B[dri3_swap_buffers]
    C[dri3_swap_buffers_with_damage]
    D[glXSwapBuffers]
    E[dri3_swap_buffers]
    F[loader_dri3_swap_buffers_msc]
    A -->|platform_x11_dri3.c| B
    B --> C
    C -->|rects=NULL,n_rects=0| F
    D -->|dri3_glx.c| E
    E --->|rects=NULL,n_rects=0| F

送显

如果平台的窗口系统(Winsys)是X11, 则送显主要是通过 Present 扩展完成的。这个与Xserver 的交互过程是通过 present_event 完成的

1
2
3
4
5
6
7
typedef struct present_event {
present_event_ptr next;
ClientPtr client;
WindowPtr window;
XID id;
int mask;
} present_event_rec;

以上无论是 loader_dri3_wait_for_msc() 还是 loader_dri3_wait_for_sbc(), 当所等待的条件满足后,都会更新(dri3_handle_present_event())当前client 的状态(UST, MSC, SBC), 整个过程是一种同步,也是一种协商。

DRI2 Throttle

这个扩展原来好像是控制 DRI client 能同时并发地渲染多少个 RenderBuffer (或者说多少帧)的,因为原来 mesa 里有一个 PIPE_CAP_MAX_FRAMES_IN_FLIGHT, 因为这个值后来要么是 1, 要么是 0,就被改成 PIPE_CAP_THROTTLE 了, 默认是 throttle 的, 按目前的实现,如果节流了,就是说当驱动提交了第 2 帧的渲染命令后,要等第 1 帧渲染完成 (pipe_fence_handle 是 drm_syncobj 的封装),才能继续准备第 3 帧的渲染命令。

flowchart LR
    subgraph "Frame 0"
        F1["CPU Submit 1"]
    end
    subgraph "Frame 1"
        F2["CPU Submit 2"]
        R1["GPU Render Done 1 fa:fa-spinner"]
    end
    subgraph "Frame 2"
        F3["CPU Submit 3"]
        R2["GPU Render Done 2 fa:fa-spinner"]
    end
    subgraph "Frame 3"
        F4["CPU Submit 4"]
        R3["GPU Render Done 3 fa:fa-spinner"]
    end

    F1 --> F2
    F2 --Block until--> R1 --> F3
    F3 --Block until--> R2 --> F4
    F4 --Block until--> R3

Throttle 的效果是 CPU 在提交后一帧的渲染命令后,要等(所谓“等”就是主线程调用 drmSyncobjWait() 阻塞)前一帧渲染完成后才唤醒继续准备下一帧数据,整个过程实际上是节流 CPU, 是让生产者-消费者中的生产者(CPU) 慢一点。所以 DRI2 Throttle 的本意就是在 GPU 渲染任务比较重(延迟大)的时候,CPU 这边没必要“玩命”准备数据,否则反而会导致比如过多消耗显存等其它问题。

Mesa 中对 Throttle 的实现很有技巧性。因为这里“等”的是上一帧数据提交给内核后,drm_gpu_scheduler 为这个 job 创建的一个 dma_fence, 在提交时,userspace 会给 kernel 一个 syncobj (当一个 fence 容器用,意思是 gpu scheduler 创建好 fence 后 attach 到这个 syncobj, userspace 就可以通过 drmSyncobjWait() 阻塞式地等 syncobj 包含的 fence 是否被 signaled)。 因为 fence 是在渲染命令 push 到 drm_gpu_scheduler 后内核才创建的, 所以提交时带下去的 syncobj 用来装当前产生的 fence,如果你需要在下一帧数据提交后等上一帧的 fence, 你需要在当前帧提交后,创建一个新的 syncobj 来存当前帧的 fence,这样就可以在下一帧提交后, 用这个新的 syncobj 来等上一帧的 fence 了。 相当于同一个 fence 放在两个不同的容器里用

Modifiers

向 X server 查询并获取显示/渲染设备所支持的 modifiers 是执行 __DRIimageExtension::createImage() 的一个准备工作。但 createImage() 允许modifiers 为空,此情况下让驱动来选一个合适的纹理图像内存布局。

1
2
3
4
5
6
__DRIimage *
(*createImage)(__DRIscreen *screen,
int width, int height, int format,
const uint64_t *modifiers, const unsigned int modifier_count,
unsigned int use,
void *loaderPrivate);

与 X server 交互的过程包括 3 步:

参考