基于 SPICE 协议的硬编推流整合方案在云游戏中的应用

背景

随着虚拟化技术如模拟器,容器化等技术等发展,在安卓云游戏/云手机场景中,可以在服务宿主侧虚拟出更多更小颗粒度的Android实例。其中比较核心的技术是图形虚拟化技术,如何最大限度利用宿主侧的GPU资源进行渲染和编码,不考虑软编等利用CPU资源进行渲染编码是因为效率带来的延迟问题。

Linux图形栈

先看一个比较通用的linux图形栈:

X协议:比较早的协议,Xserver直接管理GPU内的framebuffer和XClient提交命令,通过XClient(Xlib或XCB)向Xserver(Xorg)提交相关命令实现,且有很多扩展协议,但是弊端需要一个额外的WindowsManager来处理多个应用。目前已经被Wayland这种扩展协议取代,composer处理输入,窗口,合成显示等功能。

GLX:因为是用来做间接渲染,做了两个工作:1)将OpenGL和XwindowAPI绑定2)通过Xserver转发GL的调用。本质还是X协议那一套。

FBdriver:历史遗留显示子系统,提供了Framebuffer获取,图像操作原语,电源管理等功能。

OpenGL:统一的3D图形渲染API接口,各主流厂商(Intel、Nvidia、AMD、Qualcomm等)都支持的接口,主流实现的是开源的mesa。Mesa3D是其最主流的开源实现,值得注意的是Mesa不仅支持OpenGL,还支持Vulkan,Direct3D等渲染API。

DRM:DirectReringManager,目前主流的GPU显示子系统,用户态使用libDRM的DRMAPI来操作DRM设备,对GPU通过ioctl等标准文件操作来通信,实现:

framebuffer管理。

用户态抽象渲染能力:如BufferObject管理,GPU作业命令提交等,一般和具体driver相关。

VirtualDriver支持:包含vmwgfx(VMware桥接设备)和virgl(Virto桥接设备)

PrimeZero-copymemory,buffer通过用户态的fd文件描述符代表了实际显存中的DMAbuffer,通过PrimeAPI导出FD,可以在IPC之间传递。

包含Wayland比较主流的所有图像栈变得异常复杂:

每种应用的图像数据流都比较复杂,但大致流线是:应用(显示Client)-(显示Server)-OpenGL/EGL-Mesa3D-libDRM-(内核)DRM-GPU驱动。

Android图形栈

以SurfaceFlinger为核心,维护了所有app窗口的交叠覆盖关系:

OpenGLES:2D/3D渲染走的路径,使用drm所有功能进行绘制渲染。

GrallocFB:使用drm为app提供显存管理等功能。

HWComposer:调用openGL窗口合成RGB或者YUV,实现屏幕绘制。

综合Linux图形栈和Android图形栈可以发现在底层都是基于drm实现,实现硬编方案的核心思想就是渲染和编码都利用宿主侧的GPU,并且渲染和编码Zero-copy,所以有两类技术:

virto-gpu技术将OpenGLES命令导出(virgl)之后再反过来调用宿主侧的virglrerer,又将其翻译回OpenGL和GLSL,然后再调用宿主的OpenGL,这部分技术代表是Qemu方案。使用假的抽象GPU。在抽象层GPU层进行拦截,并调用宿主侧的GPU。

直接导出DRM句柄,利用DRM的Zero-copy的特性进行渲染和编码,渲染和编码通过IPC技术传递fd,这部分代表是AiC(AndroidinContainer)容器化技术。

以上两类技术由于最终都是drm图像buffer,故都可以通过IPC技术在渲染进程和编码进程之间通过IPC传递。渲染进程一般在容器/模拟器内,编码进程一般在容器外。

多媒体编解码相关

以上图形栈涉及显示和渲染,在云游戏的场景中,还需考虑编码设计的技术栈。就编码而言:

在Linux中ffmepg或者gstreamer等标准多媒体框架对上封装了应用接口,对下对接了硬件提供编解码如CUDANVEnc接口或VAAPI接口。

在Android中使用OMX作为其多媒体框架,MediaCodec驱动对接vor驱动来实现硬编解码能力。

如果想要在Android内使用硬件编解码,要么实现一套OMX到ffmpeg的转换翻译,要么厂商对接实现OMX的vor驱动,否则很难在Android(容器或模拟器中)内硬件编码。比较合理的方式是导出libdrm的FD,渲染和编码在不同的进程中,编码选择在host中调用ffmepg或者ver的编码API进行编码,进而完成整个推流。

方案

硬编目前精力放在处理进程间传递图像primeFD,还有相应的音频,双向input通信等。采用统一的Spice协议或者改造的Spice协议统一Android虚拟化和容器化整合方案。

spice协议

SPICE,SimpleProtocolforIndepentComputingEnvironment,是Redhat公司开源的一套远程桌面虚拟化协议,旨在提供商业级别的远程桌面体验。Spice协议具有如下优点:

开源:易于扩展和功能定制;

跨平台:Windows/Linux/MacOS平台全兼容;

支持外接设备:除常用USB设备外,打印机和扫描仪等设备也能在远程使用;

更小的带宽占用:Spice里内置图像压缩算法,有效减少数据传输时的带宽占用;

更安全的数据传输:Spice可以使用OpenSSL加密传输数据。

概述

包含四个部分:协议、客户端侧、服务端侧和虚拟机侧。其中:

协议:是客户端侧、服务端侧和虚拟机侧三个部分交互时所遵循的准则;

客户端:负责接收并转换虚拟机数据,以及发送用户输入数据到虚拟机,从而使得用户能够与虚拟机进行交互;

服务端:集成在Hypervisor内部的一个用户层组件,使得Hypervisor(如QEMU)支持Spice协议;

虚拟机侧:指所有部署在虚拟机内部的必需组件,如QXL驱动、SpiceAgent等。

图像流

上图示意了整个图像从GuestOS到客户端图像传输通路,其中:

QEMU:虚拟机环境,目前使用

GuestOS:运行在虚机中的操作系统

ClientOS:运行在host侧的应用程序

GDI/X:GraphicsDeviceInterface,图像引擎,图像栈提供的显示接口(如mesa)

QXL:设备驱动,提供了套动态设备需要客户机的QXL驱动来发挥全部作用。但是,当没有驱动的时候,标准的VGA也能支持该设备。这个模式还能显示虚拟机启动的引导阶段。QXL设备通过命令和指针环,显示中断,指针事件,I/O端口来与驱动交互。

QXL设备的其他功能包括:

初始化和映射设备ROM,RAM和VRAM到物理内存

映射I/O端口,处理读写来管理:区域更新,命令,指针通知,IRQ更新,模式设置,设备重置,记录日志等。

环-初始化和维护命令和指针环,从环获取命令和指针命令,等待通知。维护资源环。

使用QXLWorker接口与相应的redworker通信,这是在reddispatcher中实现的,它把设备调用翻译为消息写到redworker通道,或者从redworker通道中读取消息。

注册QXL接口来使worker能与设备通信。这个接口包括PCI信息和功能(如依附一个worker,从环中获取显示和指针命令,显示和指针通知,模式改变通知等)。

定义支持QXL模式和改变当前模式(如VGA:所有监听器反映一个单一设备)

处理在VGA模式中显示的初始化,更新,改变尺寸和刷新。

VDagent命令流

SpiceServer

Spice协议改造

SpiceClient收到SpiceServer端发过来的main,display,playback等通道后:

显示通道在默认的display上调用GTKwidget相关组件将图像画在屏幕上。

获取playback音频数据,通过gstreamer的pipeline,调用alsa播放音频。

其他鼠标键盘等处理,不作任何处理。

为适合我们的推流改造如下:

对原协议改动比较大的:

DisplayChannel通道,这部分在获取到FD之后,原本画在GTK的流程通过HwFrame适配模块,转换成RTC编码所需的数据源(YUV或者RGBA)。

MainChannel通道增加VDAgent类型增加自定义类型传输RTC远程调用指令,反向通过封装将RTC的事件和DataChannel传递给GameService(游戏管理服务进程)。

Spice协议抓包

可以通过socat等工具代理domainsocket来分析spice协议,对一个完整的Spice协议交互流程,通过TCPdump抓取wireshark日志如下:

先通过mainchannel建立连接,认证,然后依次建立SpiceDisplay,SpicePLAYBACK,SPICERECORD,SPICEINPUT等通道,最后通过各通道发送特定的消息。

MainChannel的VDAgent通道,在CLIPBOARD和FILE_XFER之外添加VD_AGENT_VENDOR_DATA,为远程gRPC调用,接收android侧的封装数据。

DisplayChannel,

GL_SCANOUT_UNIX,屏幕初始化/改分辨率后的消息,一般用在初始化的时候。

GL_DRAW_DONE,当屏幕内容有变化的时候传递此消息,可以认为是每一幅安卓画面。

PlaybackChannel,android系统的声音消息,如音量变化,声音开始与停止等。

QEMU方案

QEMU方案可以直接复用社区的qemu+kvm方案,除了针对不同硬件导出不同的fd之外。

AiC容器

方案参数

在整个整合方案中,有如下因素和参数需考虑:

模拟器或者容器环境区分。如果导出的fd在模拟器和容器方案一致,不需要区分,否则需要通过环境变量或者传入启动参数来区分。

DRMdevice指定。在携带GTK的版本中需要指定Display的device,移除Xorg依赖后需指定Rernode。

编码显卡硬件指定,由于不同硬件编码不同,在编码模块需要通过当前硬件信息来确定编码方式。

图形导出

从虚机或者容器导出,有两种类型的图形显存的fd:

KHR_STREAM

渲染到宿主侧的surface,suface导出EGLSTREAM,通过eglCreateStreamKHR和eglGetStreamFileDescriptorKHR导出对应的EGLStreamKHR文件描述符,适用于NVIDIA显卡。消费侧通过eglStreamConsumerAcquireKHR导出对应的stream,但编码不能直接使用stream类型,CUDA提供了OpenGL与CUDA互操作API,将texture或者rerbuffer绑定CUDA资源之后,CuGraphicsSubResourceGetMappedArray映射出CUarray指针供编码器使用。

MESA_IMAGE

渲染到宿主侧的texture,texture导出为DMAbuffer,通过eglExportDMABUFImageQueryMESA,eglExportDMABUFImageMESA导出,适用于AMD/Intel显卡。消费侧通过此创建EGLImage,并绑定2D纹理,将此纹理的textureID传递给编码器VAAPI通过此编码器进行编码。

优化与演进代码重构

随着支撑的方案类型增加,整个工程在满足功能情况下逐渐难以维护,通过C++重写各个模块,将HwFrame模块抽象,对日志/SRE各模块划分重构,将工程模块化。

移除Xorg

Xorg作为X(11)协议中的Server的实现,SpiceClient的通过调用GTKAPI端做client,存在弊端:

默认会将导出的fd,通过GTKwidget画在默认的Display上,但是实际推流过程并不需要这个步骤。

部署Xorg也增加了复杂度。

需要将依赖和GTK的组件移除,降低组件依赖复杂性和性能消耗。

具体而言:

Displaychannel相关的GTKwidget依赖移除,。

替换原有Display,对Nvidia,getPlatformDisplayEXT(EGL_PLATFORM_DEVICE_EXT,(EGLNativeDisplayType)dev[num],NULL)导出。

替换原有Display,对VAAPI的AMD或者Intel显卡,由于使用的mesa图形栈,getPlatformDisplayEXT(EGL_PLATFORM_GBM_MESA导出,需要注意的一点是在VAAPI接口中,将初始化用的Display换成DRM导出。

elseintvaapi_init(intdrm_fd){va_display=(uint64_t)vaGetDisplayDRM(drm_fd);g_message("drm_fd:%dva_dpy:%p",drm_fd,va_display);#ifintmajor_ver,minor_ver;va_status=vaInitialize(va_display,major_ver,minor_ver);return0;}
移除gstreamer

音频的pipeline使用了gstreamer,这部分依赖可以去掉。

图形转换优化

总体想法就是图像的Zero-copy,减少在CPU与GPU之间的拷贝与图形格式之间转换。

编码卡支持

支持主机通过PCIE外插硬件编码卡进行硬件编码。

混合硬件编码支持

总体想法就是利用host渲染能力,将渲染后的RGBA或者YUV导出给编码卡,达到最大限度利用渲染资源,提高并发路数的工作。

自升级

通过HostGameservice进程自我升级固件,不依赖整体部署pod节点镜像更新,可以灵活实现升级。

监控与SRE

对系统指标的打点和性能的监控,完善SRE等监控体系,治理进程崩溃,卡死,内测泄漏等检测。

其他关于我们参考

(computer_graphics)

版权声明:本站所有作品(图文、音视频)均由用户自行上传分享,仅供网友学习交流,不声明或保证其内容的正确性,如发现本站有涉嫌抄袭侵权/违法违规的内容。请举报,一经查实,本站将立刻删除。

相关推荐