随着虚拟化技术如模拟器,容器化等技术等发展,在安卓云游戏/云手机场景中,可以在服务宿主侧虚拟出更多更小颗粒度的Android实例。其中比较核心的技术是图形虚拟化技术,如何最大限度利用宿主侧的GPU资源进行渲染和编码,不考虑软编等利用CPU资源进行渲染编码是因为效率带来的延迟问题。
先看一个比较通用的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驱动。
以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,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模式中显示的初始化,更新,改变尺寸和刷新。
SpiceClient收到SpiceServer端发过来的main,display,playback等通道后:
显示通道在默认的display上调用GTKwidget相关组件将图像画在屏幕上。
获取playback音频数据,通过gstreamer的pipeline,调用alsa播放音频。
其他鼠标键盘等处理,不作任何处理。
为适合我们的推流改造如下:
对原协议改动比较大的:
DisplayChannel通道,这部分在获取到FD之后,原本画在GTK的流程通过HwFrame适配模块,转换成RTC编码所需的数据源(YUV或者RGBA)。
MainChannel通道增加VDAgent类型增加自定义类型传输RTC远程调用指令,反向通过封装将RTC的事件和DataChannel传递给GameService(游戏管理服务进程)。
可以通过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+kvm方案,除了针对不同硬件导出不同的fd之外。
在整个整合方案中,有如下因素和参数需考虑:
模拟器或者容器环境区分。如果导出的fd在模拟器和容器方案一致,不需要区分,否则需要通过环境变量或者传入启动参数来区分。
DRMdevice指定。在携带GTK的版本中需要指定Display的device,移除Xorg依赖后需指定Rernode。
编码显卡硬件指定,由于不同硬件编码不同,在编码模块需要通过当前硬件信息来确定编码方式。
从虚机或者容器导出,有两种类型的图形显存的fd:
渲染到宿主侧的surface,suface导出EGLSTREAM,通过eglCreateStreamKHR和eglGetStreamFileDescriptorKHR导出对应的EGLStreamKHR文件描述符,适用于NVIDIA显卡。消费侧通过eglStreamConsumerAcquireKHR导出对应的stream,但编码不能直接使用stream类型,CUDA提供了OpenGL与CUDA互操作API,将texture或者rerbuffer绑定CUDA资源之后,CuGraphicsSubResourceGetMappedArray映射出CUarray指针供编码器使用。
渲染到宿主侧的texture,texture导出为DMAbuffer,通过eglExportDMABUFImageQueryMESA,eglExportDMABUFImageMESA导出,适用于AMD/Intel显卡。消费侧通过此创建EGLImage,并绑定2D纹理,将此纹理的textureID传递给编码器VAAPI通过此编码器进行编码。
随着支撑的方案类型增加,整个工程在满足功能情况下逐渐难以维护,通过C++重写各个模块,将HwFrame模块抽象,对日志/SRE各模块划分重构,将工程模块化。
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等监控体系,治理进程崩溃,卡死,内测泄漏等检测。
(computer_graphics)
版权声明:本站所有作品(图文、音视频)均由用户自行上传分享,仅供网友学习交流,不声明或保证其内容的正确性,如发现本站有涉嫌抄袭侵权/违法违规的内容。请举报,一经查实,本站将立刻删除。