随着虚拟化技术如模拟器,容器化等技术等发展,在安卓云游戏/云手机场景中,可以在服务宿主侧虚拟出更多更小颗粒度的 Android 实例。其中比较核心的技术是图形虚拟化技术,如何最大限度利用宿主侧的 GPU 资源进行渲染和编码,不考虑软编等利用 CPU 资源进行渲染编码是因为效率带来的延迟问题。
Linux 图形栈先看一个比较通用的 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 等标准文件操作来通信,实现:
- framebuffer 管理。
- 用户态抽象渲染能力:如 Buffer Object 管理,GPU 作业命令提交等,一般和具体 driver 相关。
- Virtual Driver 支持:包含 vmwgfx(VMware 桥接设备)和 virgl(Virto 桥接设备)
- Prime Zero-copy memory,buffer 通过用户态的 fd 文件描述符代表了实际显存中的 DMA buffer,通过 Prime API 导出 FD,可以在 IPC 之间传递。
包含 Wayland 比较主流的所有图像栈变得异常复杂:
每种应用的图像数据流都比较复杂,但大致流线是:应用(显示 Client)->(显示 Server)->OpenGL/EGL->Mesa 3D->libDRM->(内核)DRM->GPU 驱动。
Android 图形栈
以 SurfaceFlinger 为核心,维护了所有 app 窗口的交叠覆盖关系:
- OpenGL ES:2D/3D 渲染走的路径,使用 drm 所有功能进行绘制渲染。
- Gralloc FB: 使用 drm 为 app 提供显存管理等功能。
- HWComposer: 调用 openGL 窗口合成 RGB 或者 YUV,实现屏幕绘制。
综合 Linux 图形栈和 Android 图形栈可以发现在底层都是基于 drm 实现,实现硬编方案的核心思想就是渲染和编码都利用宿主侧的 GPU,并且渲染和编码 Zero-copy,所以有两类技术:
- virto-gpu 技术将 OpenGLES 命令导出(virgl)之后再反过来调用宿主侧的 virglrenderer,又将其翻译回 OpenGL 和 GLSL,然后再调用宿主的 OpenGL,这部分技术代表是 Qemu 方案。使用假的抽象 GPU。在抽象层 GPU 层进行拦截,并调用宿主侧的 GPU。
- 直接导出 DRM 句柄,利用 DRM 的 Zero-copy 的特性进行渲染和编码,渲染和编码通过 IPC 技术传递 fd,这部分代表是 AiC(Android in Container)容器化技术。
- 以上两类技术由于最终都是 drm 图像 buffer,故都可以通过 IPC 技术在渲染进程和编码进程之间通过 IPC 传递。渲染进程一般在容器/模拟器内,编码进程一般在容器外。
以上图形栈涉及显示和渲染,在云游戏的场景中,还需考虑编码设计的技术栈。就编码而言:
- 在 Linux 中 ffmepg 或者 gstreamer 等标准多媒体框架对上封装了应用接口,对下对接了硬件提供编解码如 CUDA NVEnc 接口或 VAAPI 接口。
- 在 Android 中使用 OMX 作为其多媒体框架,MediaCodec 驱动对接 vendor 驱动来实现硬编解码能力。
如果想要在 Android 内使用硬件编解码,要么实现一套 OMX 到 ffmpeg 的转换翻译,要么厂商对接实现 OMX 的 vendor 驱动,否则很难在 Android(容器或模拟器中)内硬件编码。比较合理的方式是导出 libdrm 的 FD,渲染和编码在不同的进程中,编码选择在 host 中调用 ffmepg 或者 vender 的编码 API 进行编码,进而完成整个推流。
方案硬编目前精力放在处理进程间传递图像 prime FD,还有相应的音频,双向 input 通信等。采用统一的 Spice 协议或者改造的 spice 协议统一 Android 虚拟化和容器化整合方案。
spice 协议SPICE,Simple Protocol for Independent Computing Environment,是 Redhat 公司开源的一套远程桌面虚拟化协议,旨在提供商业级别的远程桌面体验。Spice 协议具有如下优点:
- 开源:易于扩展和功能定制;
- 跨平台:Windows/Linux/Mac OS 平台全兼容;
- 支持外接设备:除常用 USB 设备外,打印机和扫描仪等设备也能在远程使用;
- 丰富的媒体支持:包括视频、音频、图像;
- 更小的带宽占用:Spice 里内置图像压缩算法,有效减少数据传输时的带宽占用;
- 更安全的数据传输:Spice 可以使用 OpenSSL 加密传输数据。
包含四个部分:协议、客户端侧、服务端侧和虚拟机侧。其中:
- 协议:是客户端侧、服务端侧和虚拟机侧三个部分交互时所遵循的准则;
- 客户端:负责接收并转换虚拟机数据,以及发送用户输入数据到虚拟机,从而使得用户能够与虚拟机进行交互;
- 服务端:集成在 Hypervisor 内部的一个用户层组件,使得 Hypervisor(如 QEMU)支持 Spice 协议;
- 虚拟机侧:指所有部署在虚拟机内部的必需组件,如 QXL 驱动、Spice Agent 等。
图像流
上图示意了整个图像从 Guest OS 到客户端图像传输通路,其中:
- QEMU:虚拟机环境,目前使用
- Guest OS:运行在虚机中的操作系统
- Client OS:运行在 host 侧的应用程序
- GDI/X:graphics Device Interface,图像引擎,图像栈提供的显示接口(如 mesa)
- QXL:设备驱动,提供了套动态 设备需要客户机的 QXL 驱动来发挥全部作用。但是,当没有驱动的时候,标准的 VGA 也能支持该设备。这个模式还能显示虚拟机启动的引导阶段。QXL 设备通过命令和指针环,显示中断,指针事件,I/O 端口来与驱动交互。
QXL 设备的其他功能包括:
- 初始化和映射设备 ROM,RAM 和 VRAM 到物理内存
- 映射 I/O 端口,处理读写来管理:区域更新,命令,指针通知,IRQ 更新,模式设置,设备重置,记录日志等。
- 环-初始化和维护命令和指针环,从环获取命令和指针命令,等待通知。维护资源环。
- 使用 QXLWorker 接口与相应的 red worker 通信,这是在 red dispatcher 中实现的,它把设备调用翻译为消息写到 red worker 通道,或者从 red worker 通道中读取消息。
- 注册 QXL 接口来使 worker 能与设备通信。这个接口包括 PCI 信息和功能(如依附一个 worker,从环中获取显示和指针命令,显示和指针通知,模式改变通知等)。
- 定义支持 QXL 模式和改变当前模式(如 VGA:所有监听器反映一个单一设备)
- 处理在 VGA 模式中显示的初始化,更新,改变尺寸和刷新。
Spice Server
Spice 协议改造
Spice Client 收到 Spice Server 端发过来的 main,display,playback 等通道后:
- 显示通道在默认的 display 上调用 GTK widget 相关组件将图像画在屏幕上。
- 获取 playback 音频数据,通过 gstreamer 的 pipeline,调用 alsa 播放音频。
- 其他鼠标键盘等处理,不作任何处理。
为适合我们的推流改造如下:
对原协议改动比较大的:
- Display Channel 通道,这部分在获取到 FD 之后,原本画在 GTK 的流程通过 HwFrame 适配模块,转换成 RTC 编码所需的数据源(YUV 或者 RGBA)。
- Main Channel 通道增加 VDAgent 类型增加自定义类型传输 RTC 远程调用指令,反向通过封装将 RTC 的事件和 DataChannel 传递给 GameService(游戏管理服务进程)。
可以通过 socat 等工具代理 domain socket 来分析 spice 协议,对一个完整的 Spice 协议交互流程,通过 TCP dump 抓取 wireshark 日志如下:
先通过 main channel 建立连接,认证,然后依次建立 Spice Display, Spice PLAYBACK, SPICE RECORD, SPICE INPUT 等通道,最后通过各通道发送特定的消息。
这里重点关注以下:
- Main Channel 的 VDAgent 通道,在 CLIPBOARD 和 File_XFER 之外添加 VD_AGENT_VENDOR_DATA,为远程 gRPC 调用,接收 android 侧的封装数据。
- Display Channel,
- GL_SCANOUT_UNIX, 屏幕初始化/改分辨率后的消息,一般用在初始化的时候。
- GL_DRAW_DONE,当屏幕内容有变化的时候传递此消息,可以认为是每一幅安卓画面。
- Playback Channel,android 系统的声音消息,如音量变化,声音开始与停止等。
QEMU 方案
QEMU 方案可以直接复用社区的 qemu kvm 方案,除了针对不同硬件导出不同的 fd 之外。
AiC 容器
相比于虚拟化,容器化的特殊之处在于 qemu 已经集成 Spice Server 组件,虚拟化的容器需要容器外编码的话需要导出音频,视频和控制通道,然后实现一套类似 Spice 协议的架构,为统一兼容性通过增加下图的转发模块 Adapter/SpiceServer,将 IPC 通道再次转发至 Spice 通道,实现 QEMU/AiC 方案的统一。
方案参数
在整个整合方案中,有如下因素和参数需考虑:
- 模拟器或者容器环境区分。如果导出的 fd 在模拟器和容器方案一致,不需要区分,否则需要通过环境变量或者传入启动参数来区分。
- DRM device 指定。在携带 GTK 的版本中需要指定 Display 的 device,移除 Xorg 依赖后需指定 Render node。
- 编码显卡硬件指定,由于不同硬件编码不同,在编码模块需要通过当前硬件信息来确定编码方式。
从虚机或者容器导出,有两种类型的图形显存的 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 各模块划分重构,将工程模块化。
移除 XorgXorg 作为 X(11)协议中的 Server 的实现,Spice Client 的通过调用 GTK API 端做 client,存在弊端:
- 默认会将导出的 fd,通过 GTK widget 画在默认的 Display 上,但是实际推流过程并不需要这个步骤。
- 部署 Xorg 也增加了复杂度。
需要将依赖和 GTK 的组件移除,降低组件依赖复杂性和性能消耗。
具体而言:
- Display channel 相关的 GTK widget 依赖移除,。
- 替换原有 Display,对 Nvidia,getPlatformDisplayEXT(EGL_PLATFORM_DEVICE_EXT, (EGLNativeDisplayType)dev[num], NULL)导出。
- 替换原有 Display,对 VAAPI 的 AMD 或者 Intel 显卡,由于使用的 mesa 图形栈,getPlatformDisplayEXT(EGL_PLATFORM_GBM_MESA导出,需要注意的一点是在 VAAPI 接口中,将初始化用的 Display 换成 DRM 导出。
#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对系统指标的打点和性能的监控,完善 SRE 等监控体系,治理进程崩溃,卡死,内测泄漏等检测。
其他整个云游戏的视频流硬编码方案的实现和上线部署离不开跨部门的合作,再次感谢一起将整个方案从开始设计到到上线的内部兄弟团队如基础系统部门 STE,多媒体 RTC 等部门,通过团队协作推动整个方案上线以及后续线上持续优化和治理。
关于我们作为字节跳动的视频中台部门,视频架构支持了字节全系产品的点播、直播、实时通信、图片、云游戏、多媒体业务发展,目标成为业界多媒体解决方案领导者,构建极致的视频技术/产品服务体验!
视频架构-设备与服务团队聚焦多媒体 IoT/5G 相关领域,孵化能够赋能业务的新场景和核心技术,打造极致的、软硬件结合的技术解决方案,上线了云游戏、云手机、视联网、多屏互动等多款服务,支持了抖音、西瓜等众多内部产品,同时也通过火山引擎提供 toB 服务。欢迎更多同学加入我们,构建行业顶尖的视频创新技术,联系luxuguang@bytedance.com 注明“设备与服务方向”。
参考- https://en.wikipedia.org/wiki/Direct_Rendering_Manager
- https://en.wikipedia.org/wiki/Direct_Rendering_Infrastructure
- https://en.wikipedia.org/wiki/Mesa_(computer_graphics)
- https://source.android.com/devices/graphics
- https://www.opengl.org/
- https://www.mesa3d.org/
- https://www.spice-space.org/
- https://www.qemu.org/
- https://docs.nvidia.com/cuda/cuda-driver-api/index.html