作者 | 路绪光 随着虚拟化技术如模拟器 ,基于容器化等技术等发展,协戏中在安卓云游戏/云手机场景中,议的硬编云游用可以在服务宿主侧虚拟出更多更小颗粒度的推流 Android 实例 。其中比较核心的整合技术是图形虚拟化技术,如何最大限度利用宿主侧的基于 GPU 资源进行渲染和编码,不考虑软编等利用 CPU 资源进行渲染编码是协戏中因为效率带来的延迟问题。 先看一个比较通用的议的硬编云游用 linux 图形栈: X 协议
:比较早的建站模板协议,X server 直接管理 GPU 内的推流 framebuffer 和 X Client 提交命令,通过 XClient(Xlib 或 XCB)向 Xserver(Xorg)提交相关命令实现,整合且有很多扩展协议,基于但是协戏中弊端需要一个额外的 Windows Manager 来处理多个应用。目前已经被 Wayland 这种扩展协议取代,议的硬编云游用composer 处理输入
,推流窗口,整合合成显示等功能
。 GLX:因为是用来做间接渲染,做了两个工作
:1)将 OpenGL 和 X window API 绑定 2)通过 X server 转发 GL 的源码库调用
。本质还是 X 协议那一套。 FB driver:历史遗留显示子系统,提供了 Framebuffer 获取
,图像操作原语,电源管理等功能 。 OpenGL:统一的 3D 图形渲染 API 接口,各主流厂商(Intel
、 Nvidia 、AMD、Qualcomm 等)都支持的接口,主流实现的是云计算开源的 mesa。Mesa 3D 是其最主流的开源实现,值得注意的是 Mesa 不仅支持 OpenGL
,还支持 Vulkan, Direct 3D 等渲染 API 。 DRM :Direct Rendering Manager, 目前主流的 GPU 显示子系统,用户态使用 libDRM 的 DRM API 来操作 DRM 设备,对 GPU 通过 ioctl 等标准文件操作来通信 ,实现: 包含 Wayland 比较主流的所有图像栈变得异常复杂
: 每种应用的图像数据流都比较复杂,但大致流线是 :应用(显示 Client)->(显示 Server)->OpenGL/EGL->Mesa 3D->libDRM->(内核)DRM->GPU 驱动。 以 SurfaceFlinger 为核心,维护了所有 app 窗口的交叠覆盖关系: 综合 Linux 图形栈和 Android 图形栈可以发现在底层都是基于 drm 实现,实现硬编方案的核心思想就是渲染和编码都利用宿主侧的 GPU,并且渲染和编码 Zero-copy,所以有两类技术: 以上图形栈涉及显示和渲染,在云游戏的场景中
,还需考虑编码设计的技术栈
。就编码而言 : 如果想要在 Android 内使用硬件编解码 ,要么实现一套 OMX 到 ffmpeg 的转换翻译,要么厂商对接实现 OMX 的 vendor 驱动,否则很难在 Android(容器或模拟器中)内硬件编码
。比较合理的方式是导出 libdrm 的 FD,渲染和编码在不同的进程中,编码选择在 host 中调用 ffmepg 或者 vender 的编码 API 进行编码,进而完成整个推流。 硬编目前精力放在处理进程间传递图像 prime FD,还有相应的音频,双向 input 通信等。采用统一的 Spice 协议或者改造的 Spice 协议统一 Android 虚拟化和容器化整合方案。 SPICE,Simple Protocol for Independent Computing Environment ,是 Redhat 公司开源的一套远程桌面虚拟化协议
,旨在提供商业级别的远程桌面体验
。Spice 协议具有如下优点
: 概述 包含四个部分:协议、客户端侧、服务端侧和虚拟机侧。其中: 图像流 上图示意了整个图像从 Guest OS 到客户端图像传输通路
,其中
: QXL 设备的其他功能包括: VDagent 命令流 Spice Server Spice 协议改造 Spice Client 收到 Spice Server 端发过来的 main ,display
,playback 等通道后: 为适合我们的推流改造如下
: 对原协议改动比较大的
: Spice 协议抓包 可以通过 socat 等工具代理 domain socket 来分析 spice 协议,对一个完整的 Spice 协议交互流程
,通过 TCP dump 抓取 wireshark 日志如下: 先通过 main channel 建立连接 ,认证,然后依次建立 Spice Display, Spice PLAYBACK, SPICE RECORD, SPICE INPUT 等通道,最后通过各通道发送特定的消息
。 这里重点关注以下: QEMU 方案可以直接复用社区的 qemu+kvm 方案,除了针对不同硬件导出不同的 fd 之外 。 相比于虚拟化,容器化的特殊之处在于 qemu 已经集成 Spice Server 组件,虚拟化的容器需要容器外编码的话需要导出音频,视频和控制通道,然后实现一套类似 Spice 协议的架构 ,为统一兼容性通过增加下图的转发模块 Adapter/SpiceServer,将 IPC 通道再次转发至 Spice 通道,实现 QEMU/AiC 方案的统一。 在整个整合方案中,有如下因素和参数需考虑: 从虚机或者容器导出
,有两种类型的图形显存的 fd: KHR_STREAM 渲染到宿主侧的 surface
,suface 导出 EGLSTREAM,通过eglCreateStreamKHR和eglGetStreamFileDescriptorKHR导出对应的 EGLStreamKHR 文件描述符,适用于 NVIDIA 显卡。消费侧通过eglStreamConsumerAcquireKHR导出对应的 stream
,但编码不能直接使用 stream 类型, CUDA 提供了 OpenGL 与 CUDA 互操作 API ,将 texture 或者 render buffer 绑定 CUDA 资源之后,CuGraphicsSubResourceGetMappedArray映射出 CUarray 指针供编码器使用。 MESA_IMAGE 渲染到宿主侧的 texture,texture 导出为 DMA buffer,通过eglExportDMABUFImageQueryMESA ,eglExportDMABUFImageMESA导出 ,适用于 AMD/Intel 显卡
。消费侧通过此创建 EGLImage, 并绑定 2D 纹理,将此纹理的 textureID 传递给编码器 VAAPI 通过此编码器进行编码
。 随着支撑的方案类型增加,整个工程在满足功能情况下逐渐难以维护
,通过 C++重写各个模块,将 HwFrame 模块抽象 ,对日志/SRE 各模块划分重构
,将工程模块化 。 Xorg 作为 X(11)协议中的 Server 的实现,Spice Client 的通过调用 GTK API 端做 client
,存在弊端: 需要将依赖和 GTK 的组件移除,降低组件依赖复杂性和性能消耗。 具体而言: 复制#if ENABLE_GTK int vaapi_init() { x11_display = XOpenDisplay(g_getenv("DISPLAY")); va_display = vaGetDisplay(x11_display); #else int vaapi_init(int drm_fd) { va_display = (uint64_t)vaGetDisplayDRM(drm_fd); g_message("drm_fd:%d va_dpy:%p", drm_fd, va_display); #endif int major_ver, minor_ver; va_status = vaInitialize(va_display, &major_ver, &minor_ver); return 0; 音频的 pipeline 使用了 gstreamer,这部分依赖可以去掉。 总体想法就是图像的 Zero-copy,减少在 CPU 与 GPU 之间的拷贝与图形格式之间转换
。 支持主机通过 PCIE 外插硬件编码卡进行硬件编码。 总体想法就是利用 host 渲染能力 ,将渲染后的 RGBA 或者 YUV 导出给编码卡 ,达到最大限度利用渲染资源 ,提高并发路数的工作。 通过 Host Gameservice 进程自我升级固件 ,不依赖整体部署 pod 节点镜像更新,可以灵活实现升级 。 对系统指标的打点和性能的监控
,完善 SRE 等监控体系,治理进程崩溃,卡死 ,内测泄漏等检测。 整个云游戏的视频流硬编码方案的实现和上线部署离不开跨部门的合作 ,再次感谢一起将整个方案从开始设计到到上线的内部兄弟团队如基础系统部门 STE,多媒体 RTC 等部门
,通过团队协作推动整个方案上线以及后续线上持续优化和治理。背景
Linux 图形栈





方案














优化与演进
代码重构