What is the QEMU
QEMU is a set of open source software that simulates processors. It is similar to Bochs and PearPC, but it has some features that the latter two do not have, such as high speed and cross-platform features. QEMU simulates the entire computer system, including the central processing unit and other peripherals. It makes it easy to test and debug the system source code. It can also be used to create multiple virtual machines on a single host.
Google uses QEMU to develop a simulator for each version while developing Android system, which greatly reduces the development cost of developers and facilitates the promotion of Android technology. Google used QEMU to simulate the arm926EJ-S piranere processor, a virtual ARM processor that is used in Android simulations. The Android emulator runs it to run the ARM926T instruction set.
Under the Device file of Android source, we can see the names of various manufacturers and a generic directory. It says that Piranere in Android provides us with virtualization of the underlying hardware, so the execution of instructions and control of hardware will be transferred to this directory when the program is executed. Under this directory, you can see that there are piranere and pirane-Opengl, and that the simulation related to drawing is transferred to this place in the call to the upper system. Such as the related calls to OpenGLES and Gralloc. Here, the drawing instruction is encoded, and data is transmitted through HostConnection. Here, a HostConnetion class is provided, and two communication modes are provided, one is QemuPipe and the other is TcpStream. There is a problem with the TcpStream implementation and it is temporarily unavailable. Here the instructions are transferred to the simulator via QemuPipe. The simulator then translates and maps the received instructions to the corresponding local draw operation.
Emulator and Android system drawing
Overall implementation flow chart
- System to simulator
For EGL, GLES1.1 and GLES2.0 simulations are transferred to the emulator here via QEMU Pipe. In the Android layer, the instructions on the upper layer are converted into a common protocol stream, and then transmitted through a high-speed channel called QEMU PIPE. This channel is realized by the kernel driver, providing high-speed bandwidth, which can be very efficient for reading and writing. When data is streamed to a device file and then retrieved by the driver. After the draw instruction protocol stream is read by the simulator.
- Emulator to RenderThread
The simulator receives the instruction protocol stream and does nothing to change it, directing the instruction directly to the Render related class.
- Android emulator instruction conversion
The Android emulator implements several transformation libraries, implementing the upper layer EGL, GLES. Translate the corresponding function call into the correct host’s desktop API call. GLX (Linux), AGL (OS X), WGL (Windows). OpenGL 2.0 to simulate GLES1.1, GLES2.0.
Android system to emulator
Under pirane-Opengl, it provides the corresponding coding classes for EGL, GLES1.1 and GLES2.0, and obtains the IOStream held by the current GL_encoder_context for each method implemented, so as to write data into the stream for communication. The connection between the Android system and the emulator is realized through HostConnection. QemuPipe is used for communication.
The Android emulator implements a special virtual device class to provide a very fast communication channel between the host system and the emulator. The way in which such a channel is opened.
-
First open the /dev/qemu_pipe device for read and write operations. Starting with Linux3.10, the device has been renamed /dev/goldfish_pipe, but the operation is the same as before.
-
Provides a zero-ending character that describes the service we want to connect to.
-
You can then communicate with it through simple read and write operations.
fd = open("/dev/qemu_pipe", O_RDWR);
const char* pipeName = "<pipename>";
ret = write(fd, pipeName, strlen(pipeName)+1);
if (ret < 0) {
//error
}
... ready to goCopy the code
Here the pipeName is the name of the service to be used
- tcp:
Provide a NAT router that is not an internal simulator. We can only use this socket for reading and writing, but cannot connect to non-local sockets.
- unix:
Open a Unix domain socket on the host
- opengles
Connect to the OpenGL ES emulated process. Right now this implementation is equal to connect to TCP: 22468, but this may change in the future.
- qemud
Connect to the qemud service in the emulator. This replaces the /dev/ttys1 connection in the older version. The code in the kernel provides an external view of qemu_PIPE, which contains how we interact with it.
Because QEMU Pipe uses bare packets to send data, it is much faster than TCP.
Implementation of communication protocol
For the transmission of instructions, instructions are encoded and decoded. Emugen, through this tool can be encoded decoding class generation. GLES1.1, GLES2.0, and EGL define some files for code generation. The files used to define the generated code are.types,.in,.attrib. The declaration for EGL is in renderControl. EGL files all start with ‘renderControl’, mostly for historical reasons, and they call the gralloc system module to manage the graphics buffer at a lower level than EGL. EGL/GLES function calls are described through specification files that describe the types, function signatures, and some of their properties. The system’s Encoder static library is built from these generated files, which contain IOStream messages that convert EGL/GLES commands into simple byte messages.
Simulator drawing
The simulator receives render instructions from the location in Android/Opengles.cpp to control the dynamic loading of the render library, and the correct initialization to build it. Host rendering of the library under the host/libs/libOpenglRender, under the simulator opengles code in charge of the dynamic load some rendering libraries.
- RendererImpl
In RendererImpl, for each new rendering client, a RenderChannel is created through a createRenderChannel, and then a RenderThread is created.
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\n");
return nullptr;
}
return channel;
}Copy the code
RenderThread related creation code
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));
}Copy the code
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) {}Copy the code
After the RenderThread is successfully created, its start method is called. Enter an infinite loop, read the instruction stream from ChannelStream, and decode the instruction stream.
ChannelStream stream(mChannel, RenderChannel::Buffer::KSmallSize);
whileInitGL (gles1_dispatch_get_proc_func, NULL) {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); ReadBufferreadBuf(kStreamBufferSize);
const int stat = readBuf.getData(&stream, packetSize); Size_t last = tinfo.m_gldec.decode (readBuf.buf(), readBuf.validData(), &stream, &checksumCalc);
if (last > 0) {
progress = true;
readBuf.consume(last); } last = tinfo.m_gl2dec.decode (readBuf.buf(), readBuf.validData(),
&stream, &checksumCalc);
FrameBuffer::getFB()->unlockContextStructureRead();
if (last > 0) {
progress = true;
readBuf.consume(last); } last = tinfo.m_rcdec.decode (readBuf.buf(), readBuf.validData(),
&stream, &checksumCalc);
if (last > 0) {
readBuf.consume(last);
progress = true; }}Copy the code
Decoding process, omit part of the code. The core processing code is preserved.
Source of instruction flow
The above instruction stream processes data from ChannelStream, which is where the analysis begins.
- ChannelStream
Let’s take a look at where our protocol stream data comes from. From the data reading and translation process, we can see that it comes from our ChannelStream, and ChannelStream is a wrapper for Channle. Let’s look at ChannelStream’s implementation. As you can see, this is a wrapper around RenderChannel, with two buffers.
- ChannelStream is implemented from IOStream
class ChannelStream final : public IOStream
ChannelStream(std::shared_ptr<RenderChannelImpl> channel, size_t bufSize);Copy the code
The following variables are declared
std::shared_ptr<RenderChannelImpl> mChannel;
RenderChannel::Buffer mWriteBuffer;
RenderChannel::Buffer mReadBuffer;Copy the code
RenderChannel is responsible for protocol data communication between Guest and Host. RenderChannel is responsible for protocol data communication between Guest and Host. ChannleStream provides some buffers that encapsulate them, making it easier to get data from them. Meanwhile, it inherits from IOStream and defines some interfaces, making it easier to call. RenderChannel’s readFromGuest and writeToGuest are finally called for reading and writing data, which provide a Buffer to facilitate reading and writing data.
- RenderChannel
Where does the data in RenderChannel come from? Following several accessors, we can see that the specific execution is handed over to mFromGuest and mToGuest, of type, respectively
BufferQueue mFromGuest;
BufferQueue mToGuest;Copy the code
By calling its push, pop method, and getting the data from it, we can now follow up on the creation and implementation of the BufferQueue.
- BufferQueue
mFromGuest(kGuestToHostQueueCapacity, mLock),
mToGuest(kHostToGuestQueueCapacity, mLock)Copy the code
The BufferQueue model is a first-in, first-out queue of Renderchannel. Buffer instances can be used between different threads. The synchronization principle is that a lock is passed in at the time of creation. The internal buffer is used by RenderChannel’s buffer. Some basic operations of queues are locked accordingly.
BufferQueue(size_t capacity, android::base::Lock& lock)
: mCapacity(capacity), mBuffers(new Buffer[capacity]), mLock(lock) {}Copy the code
We simply pass the data, determine the size of the buffer, and lock it. Four key functions are provided for reading and writing Buffer.
- tryWrite(Buffer&& buffer)
mFromGuest.tryPushLocked(std::move(buffer));Copy the code
- tryRead(Buffer* buffer)
mToGuest.tryPopLocked(buffer);Copy the code
-
writeToGuest(Buffer&& buffer)
mToGuest.pushLocked(std::move(buffer));Copy the code
-
readFromGuest(Buffer* buffer, bool blocking)
mFromGuest.popLocked(buffer);Copy the code
On the server side, we only use two functions, which are also wrapped in ChannelStream
- commitBuffer(size_t size)
mChannel->writeToGuest(std::move(mWriteBuffer));Copy the code
- readRaw(void buf, size_t inout_len)
mChannel->readFromGuest(&mReadBuffer, blocking);Copy the code
The write and read functions are used by the peer end to receive data read from our queue.
Since the Android emulator receives the draw and render instructions through the Qemu Pipe, the location where the data is initially received is the Pipe service, which is actually in the EmuglPipe, in the OpenglEsPipe file.
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;Copy the code
Get a Renderer that we mentioned above to do the instruction conversion and draw on the local platform. Then create an EmuglPipe instance. Constructor for instance creation
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); }); }Copy the code
This brings us back to Render’s createRenderChannel function originally introduced above.
OnGuestClose EmuglPipe provides several functions, onGuestPoll, onGuestRecv, onGuestSend callback for the Guest, speaking, reading and writing, such as when there is data coming call or write back, RenderChannel is called to read and write the instruction stream.
Code document location
Design document
- qemu/android/docs
- android-emugl/DESIGN
The relevant code
The system end
- 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
The simulator client
- 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
- The host/libs/Translator/GLES_CM – > the Translator for GLES 1.1 commands
- The host/libs/Translator/GLES_V2 – > the Translator for GLES 2.0 commands
-
host/libs/Translator/GLcommon -> library of common translation routines
-
The host/libs/libOpenglRender – > render library (USES all host libs above) can be 2 by the ‘the renderer program below, or directly linked into the emulator UI program.
- External/qemu/android/android – emu/android/opengl/openglEsPipe / – > qemu Pipe data reception Renderj established