Android模擬器繪製實現概述

Jensen95發表於2019-02-17

什麼是QEMU

QEMU是一套模擬處理器的開源軟體。它與BochsPearPC近似,但其具有某些後兩者所不具備的特性,如高速度及跨平臺的特性。QEMU能模擬整個電腦系統,包括中央處理器及其他周邊裝置。它使得為系統原始碼進行測試及除錯工作變得容易。其亦能用來在一部主機上虛擬數部不同虛擬電腦。

Google在開發Android系統的同時,使用qemu開發了針對每個版本的一個模擬器,這大大降低了開發人員的開發成本,便於Android技術的推廣。Google使用qemu模擬的是ARM926ej-S的Goldfish處理器,Goldfish是一種虛擬的ARM處理器,在Android的模擬環境中使用。Android模擬器通過執行它來執行arm926t指令集。

在Android原始碼的device檔案下,我們可以看到有各個廠商的名稱,還有一個generic目錄,上面提到Android中goldfish為我們提供了對於底層硬體的虛擬化,所以對於指令的執行和硬體的操控,在程式執行的時候都會轉交到這裡來,在該目錄下,可以看到有goldfish和goldfish-opengl,對於繪製相關的模擬在系統上層的呼叫中都會轉移到這裡。例如OpenGLES和gralloc的相關呼叫。在這裡會對繪製指令進行編碼,通過HostConnection來進行資料的傳輸,這裡提供了一個HostConnetion類,這裡提供了兩種通訊方式,一個是QemuPipe,一個是TcpStream的方式進行傳輸。這裡TcpStream實現存在問題,暫時不可使用。到這裡指令通過QemuPipe傳輸到模擬器。模擬器再將接收到的指令轉化對映到相應的本地的繪製操作。

模擬器和Android系統繪製

整體實現流程圖

整體實現流程圖
整體實現流程圖
  • 系統到模擬器
系統到模擬器
系統到模擬器

對於EGL,GLES1.1和GLES2.0的模擬這裡會通過QEMU Pipe的方式傳輸到模擬器。在Android層中的實現,通過將上層的指令轉化為一個通用的協議流,然後通過一個叫做QEMU PIPE的高速通道來進行傳輸,這個管道是通過核心驅動來實現,提供了高速的頻寬,可以非常高效的進行讀寫。當資料通過流寫入到裝置檔案中,然後驅動從中拿到資料之後。繪製指令協議流被模擬器讀取之後。

  • 模擬器到RenderThread
image.png
image.png

模擬器接收到指令協議流之後並沒有做改變,直接將指令導到Render相關類。

  • Android模擬器的指令轉化
模擬器指令轉化
模擬器指令轉化

Android模擬器實現多個轉化的庫,實現了上層的EGL,GLES。將相應的函式呼叫轉化為正確的宿主機的桌面API呼叫。
GLX(Linux),AGL(OS X),WGL(Windows)。OpenGL 2.0來模擬GLES1.1,GLES2.0.

Android系統到模擬器

在Goldfish-openGL下提供了對於EGL,GLES1.1,GLES2.0的相應的編碼類,對於其中實現的每一個方法獲取到當前gl_encoder_context持有的IOStream,來將資料寫入到流之中來進行通訊。對於Android系統和模擬器之間的連線是通過HostConnection來實現的。其中的通訊實現採用的是QemuPipe。

Android模擬器實現了一種特殊的虛擬裝置類來提供宿主系統和模擬器之間非常快速的通訊渠道。該種通道的開啟連線方式。

  • 首先開啟/dev/qemu_pipe裝置來進行讀和寫操作,從Linux3.10開始,裝置被重新命名為/dev/goldfish_pipe,但是和之前的操作還是一樣的。

  • 提供一個零結尾的字元創描述我們所要連線的服務。

  • 然後通過簡單的讀寫操作便可以和其進行通訊。

fd = open("/dev/qemu_pipe", O_RDWR);
const char* pipeName = "<pipename>";
 ret = write(fd, pipeName, strlen(pipeName)+1);
 if (ret < 0) {
   //error
}
 ... ready to go複製程式碼

這裡的pipeName是要使用的服務名程,這裡支援的服務有

  • tcp:

提供一個非內部模擬器的NAT router,我們只能使用這個socket進行讀寫,接受,不能夠進行連線非本地socket。

  • unix:

開啟一個Unix域socket在主機上

  • opengles

連線到OpenGL ES模擬程式,現在這個實現等於連線tcp:22468,但是未來可能會改變。

  • qemud

連線到qemud服務在模擬器內,這個取代了老版本中通過/dev/ttys1的連線方式.
在核心中程式碼,向外提供了一個對於qemu_pipe,其中包含了我們如何與其進行互動。

由於QEMU Pipe傳送資料的時候使用的是裸包,其速度要比TCP的方式快很多。

通訊協議的實現

對於指令的傳輸,要對指令進行編解碼。emugen,通過這個工具可以進行編碼解碼類的生成。在GLES1.1,GLES2.0,EGL之中定義了一些程式碼生成時,需要用到的檔案。用來定義生成程式碼的文是.types,.in,.attrib。對於EGL的宣告則是在renderControl。對於EGL的檔案都是以‘renderControl’開頭的,這個主要是歷史原因,他們呼叫了gralloc系統的模組來管理圖形緩衝區在比EGL更低的級別。
EGL/GLES函式呼叫被通過一些規範檔案進行描述,這些檔案描述了型別,函式簽名和它們的一些屬性。系統的encoder靜態庫就是通過這些生成的檔案來構建的,它們包含了可以將EGL/GLES命令轉化為簡單的byte資訊的通過IOStream進行傳送。

模擬器的繪製

模擬器接收渲染指令的位置,
在android/opengles.cpp控制了動態的裝載渲染庫,和正確的初始化,構建它。host 渲染的庫在host/libs/libOpenglRender下,在模擬器opengles下的程式碼掌管動態裝載一些渲染的庫。

  • RendererImpl

模擬器接收指令渲染的實現在RendererImpl中,對於每一個新來的渲染client,都會通過createRenderChannel來建立一個RenderChannel,然後建立一個RenderThread。

RenderChannelPtr RendererImpl::createRenderChannel() {

   const auto channel = std::make_shared<RenderChannelImpl>();
   std::unique_ptr<RenderThread> rt(RenderThread::create(
            shared_from_this(), channel));

    if (!rt->start()) {
            fprintf(stderr, "Failed to start RenderThread
");
            return nullptr;
     }

    return channel;
}複製程式碼

RenderThread相關建立程式碼

std::unique_ptr<RenderThread> RenderThread::create(
        std::weak_ptr<RendererImpl> renderer,
        std::shared_ptr<RenderChannelImpl> channel) {
    return std::unique_ptr<RenderThread>(
            new RenderThread(renderer, channel));
}複製程式碼
RenderThread::RenderThread(std::weak_ptr<RendererImpl> renderer,
                           std::shared_ptr<RenderChannelImpl> channel)
    : emugl::Thread(android::base::ThreadFlags::MaskSignals, 2 * 1024 * 1024),
      mChannel(channel), mRenderer(renderer) {}複製程式碼

在RenderThread建立成功之後,呼叫了其start方法。進入死迴圈,從ChannelStream之中讀取指令流,然後對指令流進行decode操作。

ChannelStream stream(mChannel, RenderChannel::Buffer::KSmallSize);

while(1) {
    initialize decoders
    //初始化解碼部分
    tInfo.m_glDec.initGL(gles1_dispatch_get_proc_func, NULL);     tInfo.m_gl2Dec.initGL(gles2_dispatch_get_proc_func, NULL);     initRenderControlContext(&tInfo.m_rcDec);

    ReadBuffer readBuf(kStreamBufferSize);


    const int stat = readBuf.getData(&stream, packetSize);

    //嘗試通過GLES1解碼器來解碼指令流
    size_t last = tInfo.m_glDec.decode(                 readBuf.buf(), readBuf.validData(), &stream, &checksumCalc);
    if (last > 0) {
        progress = true;
        readBuf.consume(last);
    }


    //嘗試通過GLESV2的解碼器來進行指令流
    last = tInfo.m_gl2Dec.decode(readBuf.buf(), readBuf.validData(),
                                          &stream, &checksumCalc);
    FrameBuffer::getFB()->unlockContextStructureRead();
    if (last > 0) {
        progress = true;
        readBuf.consume(last);
    }


    //嘗試通過renderControl解碼器來進行指令流的解碼
    last = tInfo.m_rcDec.decode(readBuf.buf(), readBuf.validData(),
                                         &stream, &checksumCalc);
    if (last > 0) {
        readBuf.consume(last);
        progress = true;
    }
}複製程式碼

解碼過程,省略部分程式碼。保留了核心處理程式碼。

指令流的來源

上面的指令流處理的資料從ChannelStream中來獲取,這裡從ChannelStream著手進行分析。

  • ChannelStream

我們先來看一下我們的協議流資料從何處而來,從資料讀取翻譯過程可以看出是來自我們的 ChannelStream,而ChannelStream又是對於Channle的包裝。接下來看一下ChannelStream的實現。
可以看到其是對於RenderChannel的一個包裝,同時有兩個Buffer。

  • ChannelStream 實現自IOStream
class ChannelStream final : public IOStream
    ChannelStream(std::shared_ptr<RenderChannelImpl> channel, size_t bufSize);複製程式碼

宣告瞭以下變數

std::shared_ptr<RenderChannelImpl> mChannel;
RenderChannel::Buffer mWriteBuffer;
RenderChannel::Buffer mReadBuffer;複製程式碼

ChannelStream是對於RenderChannel進行了一次包裝,對於具體的操作還是交到RenderChannel進行執行,RenderChannel負責在Guest和Host之間的協議資料通訊,然後ChannleStream提供了一些buffer在對其封裝的基礎上,更方便的獲取其中的資料,同時由於繼承自IOStream,也定義了其中的一些介面,更方便呼叫。對於資料的讀寫最終呼叫了RenderChannelreadFromGuestwriteToGuest,其提供了一個Buffer來方便進行資料的讀寫。

  • RenderChannel

RenderChannel中的資料從哪裡而來呢?跟進其幾個讀寫方法,我們便會發現,其具體的執行是交給了mFromGuestmToGuest,其型別分別為

 BufferQueue mFromGuest;
 BufferQueue mToGuest;複製程式碼

通過呼叫其push,pop方法,從中獲取資料,到此,我們可以再繼續跟進一下BufferQueue的建立和實現。

  • BufferQueue
mFromGuest(kGuestToHostQueueCapacity, mLock),
mToGuest(kHostToGuestQueueCapacity, mLock)複製程式碼

BufferQueue模型是Renderchannel的一個先進先出的佇列,Buffer例項可以被用在不同的執行緒之間,其同步原理是在建立的時候,傳遞了一個鎖進去。其內部的buffer利用就是RenderChannel的buffer。對於佇列的一些基本操作進行了相應的鎖處理。

BufferQueue(size_t capacity, android::base::Lock& lock)
        : mCapacity(capacity), mBuffers(new Buffer[capacity]), mLock(lock) {}複製程式碼

這裡只是簡單地傳遞資料,確定buffer的大小,同時為其加鎖。
對於Buffer的讀寫,這裡提供了四個關鍵函式。

  • tryWrite(Buffer&& buffer)
    mFromGuest.tryPushLocked(std::move(buffer));複製程式碼
  • tryRead(Buffer* buffer)
    mToGuest.tryPopLocked(buffer);複製程式碼
  • writeToGuest(Buffer&& buffer)

    mToGuest.pushLocked(std::move(buffer));複製程式碼
  • readFromGuest(Buffer* buffer, bool blocking)

    mFromGuest.popLocked(buffer);複製程式碼

    在伺服器這一端,我們用的到的只有兩個函式,這兩個函式也是在ChannelStream中做了封裝的,分別為

  • commitBuffer(size_t size)
    mChannel->writeToGuest(std::move(mWriteBuffer));複製程式碼
  • readRaw(void buf, size_t inout_len)
    mChannel->readFromGuest(&mReadBuffer, blocking);複製程式碼

    通過write和read函式可以看出是對端在使用的,用來接收從我們的佇列之中讀資料。

由於Android模擬器端接受繪製渲染指令是通過Qemu Pipe來接收的,所以最開始接收到資料的位置則是管道服務,其實現在EmuglPipe中,在OpenglEsPipe檔案中。

auto renderer = android_getOpenglesRenderer();
if (!renderer) {
    D("Trying to open the OpenGLES pipe without GPU emulation!");
     return nullptr;
 }
EmuglPipe* pipe = new EmuglPipe(mHwPipe, this, renderer);
if (!pipe->mIsWorking) {
      delete pipe;
       pipe = nullptr;
 }
 return pipe;複製程式碼

獲取一個Renderer也就是我們上面提到的用來進行指令轉化在本地平臺進行繪製的。然後建立一個EmuglPipe例項。
例項建立的建構函式

 EmuglPipe(void* hwPipe, Service* service,
              const emugl::RendererPtr& renderer)
        : AndroidPipe(hwPipe, service) {
   mChannel = renderer->createRenderChannel();
   if (!mChannel) {
       D("Failed to create an OpenGLES pipe channel!");
       return;
   }
   mIsWorking = true;
   mChannel->setEventCallback([this](RenderChannel::State events) {this->onChannelHostEvent(events);});
   }複製程式碼

到此回到了上面最初介紹的Render的createRenderChannel函式。

EmuglPipe提供了幾個函式onGuestClose,onGuestPoll,onGuestRecv,onGuestSend等對於Guest讀寫的回撥,當有資料到來或者要寫回的時候呼叫,這個時候就會呼叫renderChannel來進行指令流的讀寫。

程式碼文件位置

設計文件

  • qemu/android/docs
  • android-emugl/DESIGN

相關程式碼

系統端

  • system/GLESv1_enc -> encoder for GLES 1.1 commands
  • system/GLESv2_enc -> encoder for GLES 2.0 commands
  • system/renderControl_enc -> encoder for rendering control commands
  • system/egl -> emulator-specific guest EGL library
  • system/GLESv1 -> emulator-specific guest GLES 1.1 library
  • system/gralloc -> emulator-specific gralloc module
  • system/OpenglSystemCommon -> library of common routines

模擬器端

  • host/libs/GLESv1_dec -> decoder for GLES 1.1 commands
  • host/libs/GLESv2_dec -> decoder for GLES 2.0 commands
  • host/libs/renderControl_dec -> decoder for rendering control commands
  • host/libs/Translator/EGL -> translator for EGL commands
  • host/libs/Translator/GLES_CM -> translator for GLES 1.1 commands
  • host/libs/Translator/GLES_V2 -> translator for GLES 2.0 commands
  • host/libs/Translator/GLcommon -> library of common translation routines

  • host/libs/libOpenglRender -> 渲染庫 (uses all host libs above)can be used by the `renderer` program below, or directly linked into the emulator UI program.

  • external/qemu/android/android-emu/android/opengl/openglEsPipe/ — >Qemu Pipe資料接收Renderj建立

相關文章