Chapter 13: Host Rendering¶
The guest never touches a real GPU. Its OpenGL ES and Vulkan calls are serialized into a byte stream (Chapter 14), shipped across the virtio-gpu / goldfish-pipe transport, and replayed on the host by a renderer that the emulator links in as a shared library: gfxstream's host backend, historically called libOpenglRender and now built from hardware/google/gfxstream/host/. This chapter is about what happens on the host side once those bytes arrive: how the command stream is decoded, how GLES calls are translated to whatever the host can actually run (desktop GL, ANGLE, SwiftShader, mesa/lavapipe), how Vulkan is forwarded to a real host driver (including MoltenVK on macOS), and how the rendered pixels — held in host ColorBuffer objects — finally reach a window, the recorder, or the WebRTC encoder.
The central object is the FrameBuffer singleton in hardware/google/gfxstream/host/frame_buffer.h, which despite its name is really the renderer's global display state. Around it sit two emulation backends — EmulationGl and VkEmulation — a set of per-context render threads, four command decoders, and a PostWorker that puts finished frames on screen. We follow a frame from the decode loop down to the host driver and back up to the display.
13.1 The Host Renderer as a Loadable Library¶
The host renderer is not part of the QEMU binary. The emulator UI layer loads it as a shared library and talks to it through a small C++ interface, so the same renderer can serve both the QEMU-based Android Emulator and the crosvm-based Cuttlefish device (Chapter 26) through the virtio-gpu path.
The public entry points live in hardware/google/gfxstream/host/include/render-utils/. RenderLib.h and render_api.h expose initLibrary() and initRenderer(); Renderer.h is the abstract interface the rest of the emulator sees. The concrete implementation is RendererImpl in hardware/google/gfxstream/host/renderer_impl.h, which owns the render window, the render channels, and the render threads:
// Source: hardware/google/gfxstream/host/renderer_impl.h
class RendererImpl final : public Renderer {
public:
bool initialize(int width, int height, const gfxstream::host::FeatureSet& features,
bool useSubWindow);
RenderChannelPtr createRenderChannel(
gfxstream::Stream* loadStream, uint32_t virtioGpuContextId) final;
void setPostCallback(OnPostCallback onPost, void* context,
bool useBgraReadback, uint32_t displayId) final;
...
};
RendererImpl::initialize() is where the singleton FrameBuffer is created. From that point on, every guest graphics process gets a RenderChannel (for the goldfish-pipe transport) or an address-space graphics consumer (for the ring-buffer transport), and each becomes one or more RenderThreads on the host.
13.1.1 GLES vs. Vulkan as separate concerns¶
A subtle but important fact: GLES emulation and Vulkan emulation are two independent backends, selected independently. The build can even compile out GLES entirely — much of the GL-specific surface in frame_buffer.h is guarded by #if GFXSTREAM_ENABLE_HOST_GLES, though some GLenum-typed legacy methods (the Deprecated create/read/update color-buffer variants) remain outside the guard. FrameBuffer queries each backend's presence at runtime:
// Source: hardware/google/gfxstream/host/frame_buffer.h
bool hasEmulationGl() const;
bool hasEmulationVk() const;
GLES goes through a translator (Section 13.4); Vulkan is forwarded almost verbatim to a host driver (Section 13.7). The two share the ColorBuffer abstraction so that a buffer produced by one API can be displayed or read back through the other.
13.1.2 The component map¶
Host renderer top-level components and their files
flowchart TB
subgraph GUEST["Guest"]
G["GLES / Vulkan app"]
end
subgraph HOST["Host renderer (gfxstream host library)"]
RT["RenderThread<br/>render_thread.cpp"]
DEC["Decoders<br/>GLESv1 / GLESv2 / Vulkan / renderControl"]
FB["FrameBuffer singleton<br/>frame_buffer.cpp"]
EGLB["EmulationGl<br/>gl/emulation_gl.cpp"]
VKB["VkEmulation<br/>vulkan/vk_common_operations.cpp"]
CB["ColorBuffer<br/>color_buffer.cpp"]
PW["PostWorker<br/>post_worker.cpp"]
end
subgraph DRV["Host graphics stack"]
TR["GLES translator<br/>gl/glestranslator"]
HDRV["Host driver<br/>desktop GL / ANGLE / SwiftShader / Vulkan"]
end
G -->|"command stream"| RT
RT --> DEC
DEC --> FB
FB --> EGLB
FB --> VKB
EGLB --> TR
TR --> HDRV
VKB --> HDRV
FB --> CB
FB --> PW
PW -->|"pixels"| DISP["Window / encoder"]
13.2 Choosing a Rendering Backend¶
Before any frame is decoded, the emulator must decide which host backend will execute the guest's GLES and Vulkan. That decision lives in the UI/init layer, not in gfxstream itself: external/qemu/android/android-ui/modules/aemu-gl-init/src/android/opengl/emugl_config.cpp.
The user-facing -gpu <mode> flag is mapped to an internal renderer enum by emuglConfig_get_renderer():
// Source: external/qemu/android/android-ui/modules/aemu-gl-init/src/android/opengl/emugl_config.cpp
SelectedRenderer emuglConfig_get_renderer(const char* gpu_mode) {
if (!gpu_mode) {
return SELECTED_RENDERER_UNKNOWN;
} else if (!strcmp(gpu_mode, "host") || !strcmp(gpu_mode, "on")) {
return SELECTED_RENDERER_HOST;
} else if (!strcmp(gpu_mode, "swiftshader")) {
return SELECTED_RENDERER_SWIFTSHADER_INDIRECT;
} else if (!strcmp(gpu_mode, "swangle")) {
return SELECTED_RENDERER_ANGLE_INDIRECT;
} else if (!strcmp(gpu_mode, "lavapipe") || !strcmp(gpu_mode, "llvmpipe")) {
return SELECTED_RENDERER_LAVAPIPE;
}
...
}
So there are five named modes the emulator distinguishes internally:
host— translate GLES to the native desktop GL driver, forward Vulkan to the native Vulkan driver.swiftshader— software GLES via Google's SwiftShader libraries (a software GL/Vulkan renderer).swangle— software GLES via ANGLE on top of SwiftShader's Vulkan ("SwANGLE").lavapipe/llvmpipe— software rendering via mesa's lavapipe (Vulkan) / llvmpipe (GL) on Linux.error— selection failed; GPU emulation is disabled.
13.2.1 The auto decision¶
The default -gpu auto does not appear in the enum above because it is resolved earlier into one of the concrete modes. In emuglConfig_init(), auto falls back to software rendering when the host has no usable GPU, and otherwise prefers host:
// Source: external/qemu/android/android-ui/modules/aemu-gl-init/src/android/opengl/emugl_config.cpp
if (no_window || async_query_host_gpu_blacklisted() ||
!sufficientHostVulkanDriver) {
switchToSoftwareGles = true;
}
...
if (switchToSoftwareGles) {
gles_mode_selected = "swangle";
} else {
gles_mode_selected = "host";
}
Platform constraints then override the choice: on Windows swangle/llvmpipe are unavailable so software falls back to swiftshader; on macOS swiftshader/llvmpipe are unavailable so software falls back to swangle. The same function also keeps a separate GLES mode and Vulkan mode, because swangle has no Vulkan flavor and is mapped to swiftshader for Vulkan.
13.2.2 Wiring the backend into the translator¶
For everything except host, gfxstream's GLES translator must be told to run on top of a host libEGL rather than the native windowing GL. emuglConfig_setupEnv() communicates this with environment variables. All three software backends — swiftshader, llvmpipe, and swangle — trigger EGL-on-EGL mode:
// Source: external/qemu/android/android-ui/modules/aemu-gl-init/src/android/opengl/emugl_config.cpp
if (!strcmp(config->gles_backend, "swiftshader") ||
!strcmp(config->gles_backend, "llvmpipe") ||
!strcmp(config->gles_backend, "swangle")) {
system->envSet("ANDROID_EGL_ON_EGL", "1");
return;
}
... // otherwise resolve ANDROID_EGL_LIB / ANDROID_GLESv2_LIB to the backend libs
The backend libraries themselves ship under lib64/gles_swiftshader/ and lib64/gles_angle/ (note that the swangle GLES library lives in the gles_angle folder — emugl_config.cpp rewrites the name swangle to angle when resolving the library path). EmuglBackendList (.../EmuglBackendList.cpp) scans those directories and reports which backends are available.
How -gpu becomes a backend selection
flowchart TB
U["-gpu mode"] --> CFG["emuglConfig_init()"]
CFG --> A{"mode == auto?"}
A -->|yes| B{"host GPU usable?"}
B -->|no| SW["swangle / swiftshader"]
B -->|yes| HOST["host"]
A -->|no| KEEP["use requested mode"]
SW --> P{"platform fixups"}
KEEP --> P
HOST --> P
P --> ENV["set ANDROID_EGL_ON_EGL<br/>and/or ANDROID_*_LIB"]
ENV --> GFX["gfxstream host picks GLES translator backend"]
13.3 Decoding the Command Stream: The Render Thread¶
Each guest graphics context becomes a RenderThread (hardware/google/gfxstream/host/render_thread.cpp). The thread owns a ReadBuffer and loops: read bytes from the channel, hand them to the decoders, repeat until the channel closes.
The loop is interesting because a single buffer can contain a mix of Vulkan, GLESv1, GLESv2, and renderControl commands, so the thread offers the buffer to each decoder in turn and consumes whatever each one recognizes:
// Source: hardware/google/gfxstream/host/render_thread.cpp
if (tInfo->m_vkInfo) {
...
last = tInfo->m_vkInfo->m_vkDec.decode(readBuf.buf(), readBuf.validData(), ioStream,
processResources, context);
if (last > 0) { readBuf.consume(last); progress = true; }
}
...
if (tInfo->m_glInfo) {
last = tInfo->m_glInfo->m_glDec.decode( // GLESv1
readBuf.buf(), readBuf.validData(), ioStream, &checksumCalc);
if (last > 0) { progress = true; readBuf.consume(last); }
last = tInfo->m_glInfo->m_gl2Dec.decode( // GLESv2/v3
readBuf.buf(), readBuf.validData(), ioStream, &checksumCalc);
if (last > 0) { progress = true; readBuf.consume(last); }
}
...
last = tInfo->m_rcDec.decode(readBuf.buf(), readBuf.validData(), // renderControl
ioStream, &checksumCalc);
There are four decoders here: the Vulkan decoder, the GLESv1 decoder, the GLESv2/v3 decoder, and the renderControl decoder. Each is generated from .entries interface descriptions (Chapter 14) and turns wire bytes back into real function calls. The decode() methods return how many bytes they consumed; the loop keeps going as long as any decoder makes progress.
13.3.1 The driver-workaround lock¶
Note the FrameBuffer::getFB()->lockContextStructureRead() call wrapped around the GLES decode in render_thread.cpp. The comment explains the reason: on Linux with some NVIDIA drivers, calling GLES functions while another thread is creating or destroying an EGL context (glXMakeCurrent) can crash inside the driver. The renderer takes a read lock around GLES dispatch and a write lock around context create/destroy to serialize against that bug. The Vulkan decode is deliberately left outside this lock.
13.3.2 renderControl: the side-channel¶
The renderControl decoder is the renderer's own control protocol — it is how the guest creates color buffers, opens/closes them, asks for GL strings, sets swap intervals, posts frames, and queries which host features are available. Its server-side handlers live in hardware/google/gfxstream/host/render_control.cpp, and most of them call straight into the FrameBuffer singleton. rcGetGLString (Section 13.6) and rcCreateColorBuffer/rcFBPost are all renderControl entry points.
The render thread decode loop
sequenceDiagram
participant CH as Channel / RingStream
participant RT as RenderThread
participant VK as Vulkan decoder
participant GL as GLES decoders
participant RC as renderControl decoder
participant FB as FrameBuffer
participant HDRV as Host GL driver
CH->>RT: bytes
loop until no progress
RT->>VK: decode(buf)
VK->>FB: VkEmulation ops
RT->>GL: decode(buf)
GL->>HDRV: GLES via translator
RT->>RC: decode(buf)
RC->>FB: createColorBuffer / post / getGLString
end
13.4 GLES Emulation: The Translator¶
When the GLES decoders execute a call, they dispatch it through a function-pointer table (GLESv1Dispatch, GLESv2Dispatch) loaded by OpenGLDispatchLoader.cpp. Those tables do not point at the host driver directly — they point at gfxstream's GLES translator in hardware/google/gfxstream/host/gl/glestranslator/. The translator is the heart of GL host rendering: it implements the GLES 1.x/2.0/3.x API on top of whatever GL the host actually exposes, fixing up the impedance mismatches (fixed-function GLES 1.x emulated over shaders, core-profile desktop GL quirks, extension differences, format conversions).
The dispatch tables are loaded lazily and chained: the GLES dispatchers force the EGL dispatch to load first, because GL entry points are resolved through eglGetProcAddress:
// Source: hardware/google/gfxstream/host/gl/OpenGLESDispatch/OpenGLDispatchLoader.cpp
LazyLoadedGLESv2Dispatch::LazyLoadedGLESv2Dispatch() {
LazyLoadedEGLDispatch::get();
mValid = gles2_dispatch_init(&s_gles2);
}
13.4.1 EGL "on EGL" vs. native windowing¶
The translator needs an EGL implementation to create contexts and surfaces. It has two ways to get one, chosen by EglGlobalInfo:
// Source: hardware/google/gfxstream/host/gl/glestranslator/egl/egl_global_info.cpp
if (sEgl2Egl) {
m_engine = EglOS::getEgl2EglHostInstance(nullEgl);
} else {
m_engine = EglOS::Engine::createHostInstance();
}
createHostInstance() is the native path: it has platform-specific implementations for GLX on Linux (egl_os_api_glx.cpp), WGL on Windows (egl_os_api_wgl.cpp), and CGL on macOS (egl_os_api_darwin.cpp). This is what -gpu host uses — the translator sits directly on the operating system's desktop OpenGL.
getEgl2EglHostInstance() is the "EGL-on-EGL" path (egl_os_api_egl.cpp): instead of native windowing GL, the translator loads a host libEGL.so/.dll/.dylib and runs on top of that. The flag that selects it, sEgl2Egl, is set by eglUseOsEglApi(), which EmulationGl calls during init based on whether the backend wants EGL-on-EGL:
// Source: hardware/google/gfxstream/host/gl/emulation_gl.cpp
if (s_egl.eglUseOsEglApi) {
s_egl.eglUseOsEglApi(eglOnEgl, EGL_FALSE);
}
That eglOnEgl flag is ultimately driven by the ANDROID_EGL_ON_EGL=1 environment variable set in emugl_config.cpp for the swangle backend. So the same translator is reused for ANGLE and SwiftShader: those backends provide a libEGL, and the translator runs GLES-on-EGL against it.
GLES translator dispatch and backend selection
flowchart TB
DEC["GLESv2 decoder"] --> DT["GLESv2Dispatch table"]
DT --> TR["GLES translator<br/>gles_v2 / gles_cm"]
TR --> EGLINFO{"sEgl2Egl?"}
EGLINFO -->|"false, -gpu host"| NATIVE["createHostInstance:<br/>GLX / WGL / CGL"]
EGLINFO -->|"true, EGL on EGL"| OSEGL["getEgl2EglHostInstance:<br/>host libEGL"]
NATIVE --> DGL["Desktop GL driver"]
OSEGL --> AS["ANGLE, SwiftShader, or llvmpipe libEGL"]
13.5 ANGLE, SwiftShader, mesa: The GLES Substrates¶
The translator always produces GLES-on-something. What that "something" is depends on the selected backend.
13.5.1 Desktop GL (-gpu host)¶
On host mode the translator targets the platform's native desktop OpenGL through GLX/WGL/CGL. This is the fastest path but the most fragile: real drivers vary wildly in extension support and conformance, which is why so much of emulation_gl.cpp and the translator is workaround code (the Intel GLES 3.1 cap, the Imagination fast-blit disable, the NVIDIA context lock). EmulationGl::create() even re-negotiates the maximum GLES version downward for some vendors:
// Source: hardware/google/gfxstream/host/gl/emulation_gl.cpp
if (gpuVendor == gfxstream::host::GpuVendor::kIntel) {
// BUG: 435712974 ... cap the max gles to 3.0
if (!ChooseGlesVersionAndPopulateDispatch(GLES_DISPATCH_MAX_VERSION_3_0)) {
return nullptr;
}
}
13.5.2 ANGLE (-gpu swangle, and Windows fast path)¶
ANGLE (external/angle/) is "Almost Native Graphics Layer Engine." Its job is to implement OpenGL ES on top of a different host API. Per its README, it can translate ES 2.0/3.0/3.1 to Vulkan, desktop GL, Direct3D 9/11, and Metal. In the emulator it is loaded as the host libEGL/libGLESv2 that the translator runs on top of (the EGL-on-EGL path).
swangle specifically means "ANGLE backed by SwiftShader's Vulkan" — software all the way down, used when there is no usable host GPU. ANGLE's Vulkan backend renders into SwiftShader's vk_swiftshader ICD. The translator's EGL-on-EGL display can also explicitly request ANGLE platform types, though this code path is only activated when the ANDROID_EMUGL_EXPERIMENTAL_FAST_PATH=1 environment variable is set; in the normal swangle case the condition is false and the code falls through to mDisplay = mDispatcher.eglGetDisplay(EGL_DEFAULT_DISPLAY):
// Source: hardware/google/gfxstream/host/gl/glestranslator/egl/egl_os_api_egl.cpp
const EGLAttrib attr[] = {
EGL_PLATFORM_ANGLE_TYPE_ANGLE,
EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE,
EGL_EXPERIMENTAL_PRESENT_PATH_ANGLE,
EGL_EXPERIMENTAL_PRESENT_PATH_FAST_ANGLE,
EGL_NONE
};
mDisplay = mDispatcher.eglGetPlatformDisplay(EGL_PLATFORM_ANGLE_ANGLE,
(void*)EGL_DEFAULT_DISPLAY, attr);
There is also a NULL-ANGLE display (EGL_PLATFORM_ANGLE_TYPE_NULL_ANGLE) used as a no-op backend for headless/testing scenarios, requested when nullEgl is set.
13.5.3 SwiftShader (-gpu swiftshader)¶
SwiftShader is Google's pure-CPU implementation of GL ES and Vulkan. In swiftshader mode the translator runs GLES-on-EGL against SwiftShader's libEGL/libGLESv2 (shipped under lib64/gles_swiftshader/), and SwiftShader executes shaders on the CPU. It is the cross-platform software fallback on Windows and Linux (macOS uses swangle instead). The Vulkan side of software rendering also uses SwiftShader's Vulkan ICD (vk_swiftshader), selected via the ANDROID_EMU_VK_ICD=swiftshader environment variable that vk_common_operations.cpp reads.
13.5.4 mesa lavapipe / llvmpipe¶
external/mesa3d/ provides mesa's software renderers: llvmpipe (a Gallium OpenGL driver) and lavapipe (a Vulkan driver built on the same LLVM-based rasterizer). On Linux, -gpu lavapipe selects these as the software path. The tree's mesa (under src/gfxstream and src/virtio) also supplies the guest gfxstream/virtio-gpu Vulkan driver — but on the host it is the lavapipe/llvmpipe software ICD. vulkan_dispatch.cpp even has a direct-load path for it that bypasses the Vulkan loader in high-integrity mode (kLavapipeIcdJson, resolving libvulkan_lvp).
A note on external/virglrenderer/: virglrenderer is the other virtio-gpu host renderer (the OpenGL-focused "virgl" path used by plain QEMU/crosvm). The Android stack uses gfxstream rather than virgl for Android guests, but virglrenderer ships in the tree because crosvm can be built with either.
GLES substrate options behind the translator
flowchart LR
TR["GLES translator"] --> M{"backend"}
M -->|host| NGL["Native desktop GL<br/>GLX / WGL / CGL"]
M -->|swangle| ANGLE["ANGLE then SwiftShader Vulkan"]
M -->|swiftshader| SS["SwiftShader GL on CPU"]
M -->|lavapipe| MESA["mesa llvmpipe / lavapipe on CPU"]
13.6 GL Strings and Why the Guest Thinks It Has a GPU¶
A GLES app on the guest calls glGetString(GL_VENDOR/GL_RENDERER/GL_VERSION) and eglQueryString. Those calls are not answered by the host driver directly — they go through the renderControl protocol to rcGetGLString in render_control.cpp, which composes the answer from the host's real strings plus the negotiated feature set.
// Source: hardware/google/gfxstream/host/render_control.cpp
static EGLint rcGetGLString(EGLenum name, void* buffer, EGLint bufferSize) {
FrameBuffer* fb = FrameBuffer::getFB();
std::string glStr;
if (fb->hasEmulationGl()) {
glStr = fb->getGlString(name);
}
const gfxstream::host::FeatureSet& features = fb->getFeatures();
bool asyncSwapEnabled = shouldEnableAsyncSwap(features);
...
}
This is also where the host's reported extension string is augmented with the synthetic extensions that the gfxstream pipe relies on — checksum support, async swap, DMA, direct memory, host composition, and a long list of Vulkan capability flags. Many of these are gated on get_gfxstream_renderer() == SELECTED_RENDERER_HOST, because the software backends do not support them:
// Source: hardware/google/gfxstream/host/render_control.cpp
if (asyncSwapEnabled && name == GL_EXTENSIONS) {
glStr += kAsyncSwapStrV2;
glStr += " ";
if (get_gfxstream_renderer() == SELECTED_RENDERER_HOST) {
glStr += kAsyncSwapStrV3;
glStr += " ";
glStr += kAsyncSwapStrV4;
glStr += " ";
}
}
The GLES version reported back is also rewritten through replaceESVersionString() so the guest sees the version gfxstream actually negotiated (2.0, 3.0, or 3.1), regardless of what the host happens to expose. The net effect: the guest's GL stack believes it is talking to a coherent GPU with exactly the extensions gfxstream chose to advertise.
13.7 Vulkan on the Host¶
Vulkan does not go through a translator. The Vulkan decoder (hardware/google/gfxstream/host/vulkan/vk_decoder.cpp) reconstructs the guest's vkCmd*/vkCreate* calls and forwards them, after handle and memory remapping, to a real host Vulkan driver. The host-side bookkeeping (handle boxing, memory mapping, snapshot reconstruction) is vk_decoder_global_state.cpp, and the device/instance lifecycle is vk_common_operations.cpp.
13.7.1 Loading the host Vulkan loader¶
vulkan_dispatch.cpp finds and dlopens the platform Vulkan loader. The basenames are platform-specific:
// Source: hardware/google/gfxstream/host/vulkan/vulkan_dispatch.cpp
static std::vector<std::string> getPossibleLoaderPathBasenames() {
#if defined(__APPLE__)
return std::vector<std::string>{"libvulkan.dylib"};
#elif defined(__linux__)
return std::vector<std::string>{"libvulkan.so", "libvulkan.so.1"};
#elif defined(_WIN32)
return std::vector<std::string>{"vulkan-1.dll"};
...
}
It searches lib64/vulkan/ next to the program/launcher first, then the system path; an explicit override is honored via ANDROID_EMU_VK_LOADER_PATH. Once loaded, vkGetInstanceProcAddr (or vk_icdGetInstanceProcAddr for a direct ICD) populates the VulkanDispatch table.
13.7.2 Physical device selection¶
VkEmulation::create() enumerates physical devices and scores them, preferring discrete GPUs and higher API versions:
// Source: hardware/google/gfxstream/host/vulkan/vk_common_operations.cpp
const uint32_t deviceTypeScoreTable[] = {
100, // VK_PHYSICAL_DEVICE_TYPE_OTHER = 0,
1000, // VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU = 1,
2000, // VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU = 2,
500, // VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU = 3,
600, // VK_PHYSICAL_DEVICE_TYPE_CPU = 4,
};
A CPU device (SwiftShader/lavapipe) scores low but is still usable, which is exactly what software-rendering modes rely on.
13.7.3 MoltenVK on macOS¶
macOS has no native Vulkan — Apple ships only Metal. MoltenVK (external/moltenvk/) is a Vulkan implementation layered on top of Metal, exposed as a portability driver. The emulator selects it through the ANDROID_EMU_VK_ICD=moltenvk environment variable, and vk_common_operations.cpp enables the portability extensions only on Apple:
// Source: hardware/google/gfxstream/host/vulkan/vk_common_operations.cpp
#ifdef __APPLE__
std::vector<const char*> moltenVkDeviceExtNames = {
VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME,
};
std::vector<const char*> portabilityEnumerationNames = {
VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME,
};
#endif
...
const bool useMoltenVK = (vulkanIcd == "moltenvk");
When useMoltenVK is set, the renderer enables VK_KHR_portability_subset on the device and remembers mInstanceSupportsMoltenVK (queried later via supportsMoltenVk()). MoltenVK advertises itself as a portability ICD — its bundled MoltenVK_icd.json carries "is_portability_driver": true, which is why the host instance must enable VK_KHR_portability_enumeration to even see the device. The decoded guest Vulkan therefore runs Metal underneath, transparently, on Mac hosts.
13.7.4 Per-application validation-layer filtering¶
Because the guest's Vulkan calls are replayed against a real host driver, the Khronos validation layers (VVL) can be loaded on the host and will validate whatever the guest sent. For a long time that was an all-or-nothing switch: setting GFXSTREAM_USE_TESTING_VALIDATION_LAYERS validated every guest VkInstance, so the output was dominated by the framework, SystemUI, and the launcher even when the app under investigation was one game. Filtering now happens per guest application.
Three command-line options drive it, declared in external/qemu/android/emu/cmdline/include/android/cmdline-options.h:198:
// Source: external/qemu/android/emu/cmdline/include/android/cmdline-options.h
OPT_PARAM( vulkan_validation, "<mode>", "Vulkan validation mode (off|print|fail|crash)" )
OPT_PARAM( vulkan_validation_include_filter, "<filters>", "Comma-separated list of app/engine filters to include for Vulkan validation" )
OPT_PARAM( vulkan_validation_exclude_filter, "<filters>", "Comma-separated list of app/engine filters to exclude for Vulkan validation" )
startOpenglesRendererImpl() — the body android_startOpenglesRenderer() dispatches to through the ops table — copies all three into the gfxstream FeatureSet as string features (external/qemu/android/android-emu/android/opengles.cpp:487 through :513, against the declarations at hardware/google/gfxstream/host/features/include/gfxstream/host/features.h:441). Any mode other than off also force-enables VulkanDebugUtils, since the whole mechanism is built on VK_EXT_debug_utils:
// Source: external/qemu/android/android-emu/android/opengles.cpp
if (strcmp(opts->vulkan_validation, "off") != 0) {
if (!gfxstreamFeatures.VulkanDebugUtils.enabled()) {
gfxstreamFeatures.VulkanDebugUtils.setEnabled(true);
Loading the layer itself is still a process-wide, environment-variable affair. ensureVulkanValidationLayersEnabled() (hardware/google/gfxstream/host/vulkan/vulkan_dispatch.cpp:394) hunts for lib64/vulkan/layers next to the program or launcher, falls back to testlib64/layers, and pushes VK_ADD_LAYER_PATH and VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation into the environment before the loader is initialized. It runs when either the old testing variable or the new ANDROID_EMU_VVL_BEHAVIOR variable is set (:488), so the layer is present in the host process; the filtering decides who gets listened to.
The configuration is resolved once, up front, into an enum rather than re-parsed per call. VVLConfiguration::parse() (hardware/google/gfxstream/host/vulkan/vk_vvl_configuration.cpp:92) maps the mode string onto VVLBehavior — None, PrintOnly, Fail, or Crash (vk_vvl_configuration.h:14) — accepting ANDROID_EMU_VVL_BEHAVIOR when the feature is unset, and lowercases each comma-separated filter token into an unordered_set. VkEmulation::create() keeps the result only if it would ever fire:
// Source: hardware/google/gfxstream/host/vulkan/vk_common_operations.cpp
auto vvlConfig = VVLConfiguration::parse(features);
if (vvlConfig.getBehavior() != VVLBehavior::None) {
emulation->mVVLConfig.emplace(std::move(vvlConfig));
}
The per-application decision is made when the guest creates an instance. The decoder's on_vkCreateInstance reads the guest's VkApplicationInfo — the same pApplicationName/pEngineName the guest's Vulkan loader filled in — and asks the configuration whether this one is interesting (vk_decoder_global_state.cpp:1168). Matching is exact and case-insensitive against either name, an empty include list means "everything", and exclude beats include:
// Source: hardware/google/gfxstream/host/vulkan/vk_vvl_configuration.cpp
return (mIncludeFilters.empty() || mIncludeFilters.find(app) != mIncludeFilters.end() ||
mIncludeFilters.find(engine) != mIncludeFilters.end()) &&
(mExcludeFilters.find(app) == mExcludeFilters.end() &&
mExcludeFilters.find(engine) == mExcludeFilters.end());
Exact matching, not substring, is the deliberate choice the unit tests pin down: com.example.game in the include list matches COM.EXAMPLE.GAME but not com.example.game.sub (hardware/google/gfxstream/host/vulkan/vulkan_unittest.cpp:435).
When an instance matches, three things happen to the VkInstanceCreateInfo the host is about to submit. VK_EXT_debug_utils is appended to the extension list if the guest did not already ask for it; the messenger's VkDebugUtilsMessengerCreateInfoEXT is appended to the pNext chain, so instance creation itself is validated (vk_decoder_global_state.cpp:1211); and once the instance exists, a real messenger is created through vkCreateDebugUtilsMessengerEXT and stashed alongside the callback payload in InstanceInfo (vk_decoder_internal_structs.h:217). Failure to create the messenger is a warning, not an error — validation degrades to whatever the pNext chain caught. The teardown path, destroyInstanceObjects(), destroys the messenger immediately before calling vkDestroyInstance (vk_decoder_global_state.cpp:10825).
The behavior enum is consumed in the callback, which receives the per-instance VVLContext through pUserData and uses its pre-formatted appInfo string (" [App: <name>, Engine: <name>]") to tag every line:
// Source: hardware/google/gfxstream/host/vulkan/vk_vvl_configuration.cpp
GFXSTREAM_LOG_INNER(logSeverity, "VVL%s: %s", context->appInfo.c_str(), pCallbackData->pMessage);
if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) {
if (context->behavior == VVLBehavior::Crash) {
GFXSTREAM_FATAL("VVL requested CRASH on error: %s", pCallbackData->pMessage);
} else if (context->behavior == VVLBehavior::Fail) {
GFXSTREAM_ERROR("VVL requested FAIL on error: %s", pCallbackData->pMessage);
return VK_TRUE;
}
}
Enforcement is scoped to errors. Warnings, info, and verbose messages are logged and nothing more, whatever the mode. Crash aborts the emulator on the first validation error, which is what you want in a bisect. Fail returns VK_TRUE, the debug-utils protocol for "abort this call", so the layer fails the offending Vulkan entry point with VK_ERROR_VALIDATION_FAILED_EXT and the error propagates back across the stream to the guest — turning a silently-ignored validation message into something the guest app has to handle. The end-to-end suite exercises exactly that shape: it registers a log callback, issues a vkCreateBuffer with size = 0 (VUID-VkBufferCreateInfo-size-00904), and asserts that VVL output appears if and only if the VulkanValidation feature was on for that parameterization (hardware/google/gfxstream/tests/end2end/gfxstream_end2end_vvl_tests.cpp:74).
Host Vulkan forwarding path
flowchart TB
GVK["Guest Vulkan call"] --> VDEC["vk_decoder.cpp"]
VDEC --> VGS["vk_decoder_global_state.cpp<br/>handle + memory remap"]
VGS --> DISP["VulkanDispatch table"]
DISP --> LOADER["Host Vulkan loader<br/>libvulkan / vulkan-1"]
LOADER --> ICD{"selected ICD"}
ICD -->|"Linux/Win HW"| HW["GPU vendor driver"]
ICD -->|software| SS["vk_swiftshader / lavapipe"]
ICD -->|macOS| MVK["MoltenVK then Metal"]
13.8 ColorBuffers: Where Rendered Pixels Live¶
The unit of rendered output is the ColorBuffer (hardware/google/gfxstream/host/color_buffer.h). The guest allocates one through rcCreateColorBuffer (or, on the virtio-gpu path, through a RESOURCE_CREATE with a fixed handle), renders into it, and posts it for display. On the host a ColorBuffer can have a GL backing (color_buffer_gl.cpp), a Vulkan backing (color_buffer_vk.cpp), or both, created from whichever emulation backends exist:
// Source: hardware/google/gfxstream/host/color_buffer.h
static std::shared_ptr<ColorBuffer> create(gl::EmulationGl* emulationGl,
vk::VkEmulation* emulationVk,
uint32_t width, uint32_t height,
GfxstreamFormat format,
HandleType handle, Stream* stream = nullptr);
Because a buffer may be touched by both APIs (rendered by GLES, displayed or read back by Vulkan, or vice versa), ColorBuffer exposes explicit flush/invalidate operations to move content between the GL and Vulkan representations:
// Source: hardware/google/gfxstream/host/color_buffer.h
bool flushFromGl();
bool flushFromVk();
bool invalidateForGl();
bool invalidateForVk();
FrameBuffer owns the handle-to-ColorBuffer map and reference counts (createColorBuffer, openColorBuffer, closeColorBuffer). It also exposes borrowColorBufferForComposition() and borrowColorBufferForDisplay(), which hand the compositor or display path a BorrowedImageInfo describing the image in whichever API will consume it next.
13.8.1 Composition¶
Before display, multiple ColorBuffer layers (from SurfaceFlinger's HWC layers, Chapter 17) may be composited into a single target. The host has two compositors implementing the same Compositor interface: CompositorGl (gl/compositor_gl.cpp), which uses TextureDraw to blit textured quads, and CompositorVk (vulkan/compositor_vk.cpp), which uses a Vulkan graphics pipeline with GLSL shaders (compositor.vert/compositor.frag) pre-compiled to SPIR-V and embedded in the generated headers compositor_vertex_shader.h/compositor_fragment_shader.h. The renderer picks the compositor that matches the display backend.
13.9 Posting: From ColorBuffer to Screen and Encoder¶
The last step is "post": taking the final ColorBuffer and getting it to the user's eyes (or the recorder). The guest triggers this through rcFBPost, which calls FrameBuffer::post() (a host-side FrameBuffer::postWithCallback() variant exists for callers that need a completion callback). Posting always happens on a dedicated PostWorker thread (post_worker.cpp) so that the GL/Vulkan presentation work does not block the render threads. FrameBuffer::sendPostWorkerCmd() enqueues a Post command:
// Source: hardware/google/gfxstream/host/post_commands.h
enum class PostCmd {
Post = 0,
Viewport = 1,
Compose = 2,
Clear = 3,
Screenshot = 4,
...
};
There are two PostWorker subclasses for the two display backends: PostWorkerGl (post_worker_gl.cpp), whose postImpl() builds a ComposeLayer and hands it to DisplayGl::post() — ultimately a TextureDraw blit into the on-screen window surface followed by eglSwapBuffers; and PostWorkerVk, whose postImpl() drives DisplayVk, presenting through a SwapChainStateVk and vkQueuePresentKHR.
13.9.1 Two ways out: window or callback¶
FrameBuffer::initialize() takes a useSubWindow flag. There are two consumers of a posted frame:
- The on-screen sub-window — when the emulator UI creates a child window (
setupSubWindow()), thePostWorkerblits/presents directly into it. The native window handle is platform-specific (native_sub_window_*for X11, Cocoa, Win32 — the Cocoa variant isnative_sub_window_cocoa.mm). - A registered post callback — for headless use, screen recording, and WebRTC streaming, the consumer registers an
OnPostCallbackand the renderer reads the posted frame back into CPU memory and hands it over.
The callback signature, in Renderer.h, always delivers RGBA bytes:
// Source: hardware/google/gfxstream/host/include/render-utils/Renderer.h
using OnPostCallback = void (*)(void* context, uint32_t displayId,
int width, int height, int ydir,
int format, int type, unsigned char* pixels);
13.9.2 Readback¶
Reading pixels back from a GPU is the slowest part of the pipeline, so the renderer maintains a ReadbackWorker whose doNextReadback() is called on the render thread, inline inside postImpl(). setPostCallback() allocates a CPU buffer per display and enqueues setup commands to m_readbackThread (a WorkerThread that handles Init, AddRecordDisplay, DelRecordDisplay, and GetPixels commands, but not per-frame readback):
// Source: hardware/google/gfxstream/host/frame_buffer.cpp
m_onPost[displayId].img = new unsigned char[4 * w * h];
m_onPost[displayId].readBgra = useBgraReadback;
...
m_readbackThread.enqueue({ ReadbackCmd::AddRecordDisplay, displayId, nullptr, 0, w, h });
For the GL path, ReadbackWorkerGl uses pixel-buffer objects to pipeline the glReadPixels, so a frame can be read back asynchronously while the next is rendered (asyncReadbackSupported() reports whether this fast path is available). The Vulkan path reads the color buffer's VkImage back through VkEmulation. Either way the bytes land in the per-display img buffer and are handed to the OnPostCallback. On the emulator side that callback is wired up in external/qemu/android/android-ui/modules/aemu-gl-bridge/src/android/gpu_frame.cpp, feeding the display surface, the screen recorder, and the WebRTC encoder (Chapter 23).
The post and readback pipeline
sequenceDiagram
participant RC as renderControl rcFBPost
participant FB as FrameBuffer
participant PW as PostWorker thread
participant DISP as DisplayGl / DisplayVk
participant CB as OnPostCallback
RC->>FB: post(colorBuffer)
FB->>PW: enqueue PostCmd Post
par PostWorker presents async
PW->>DISP: postImpl, blit or present
DISP-->>PW: presented to sub-window
and readback on render thread
FB->>FB: doNextReadback via ReadbackWorker
FB->>CB: pixels (RGBA)
CB-->>CB: window / recorder / WebRTC encoder
end
13.10 Try It¶
The following commands let you observe backend selection and the host renderer at work. Run them from a shell where the emulator binary is on PATH, and replace @<avd> with one of your AVD names (see emulator -list-avds).
Check which accelerators and renderers the host advertises:
Force each backend and watch the selection log line vulkan_mode_selected ... gles_mode_selected ...:
# Native desktop GL + native Vulkan
emulator @<avd> -gpu host -verbose 2>&1 | grep -i "mode_selected\|renderer"
# ANGLE-on-SwiftShader software rendering
emulator @<avd> -gpu swangle -verbose 2>&1 | grep -i "mode_selected\|ANDROID_EGL_ON_EGL"
# Pure SwiftShader software rendering (Windows/Linux)
emulator @<avd> -gpu swiftshader_indirect -verbose 2>&1 | grep -i mode_selected
Inspect the GL strings the guest actually sees (these come from rcGetGLString, not the raw host driver). From an adb shell on the running device:
Point the host Vulkan path at a specific ICD or loader to confirm the selection logic in vulkan_dispatch.cpp:
# Force software Vulkan (SwiftShader) regardless of host GPU
ANDROID_EMU_VK_ICD=swiftshader emulator @<avd> -gpu host -verbose 2>&1 | grep -i vulkan
# Force MoltenVK on macOS
ANDROID_EMU_VK_ICD=moltenvk emulator @<avd> -gpu host -verbose 2>&1 | grep -i "moltenvk\|portability"
# Override which Vulkan loader is dlopen'd
ANDROID_EMU_VK_LOADER_PATH=/some/path/libvulkan.so emulator @<avd> -gpu host
Turn on the validation layers for one guest application and watch the VVL [App: ...] lines appear only for it:
# Log validation errors from one package and from ANGLE, nothing else.
emulator @<avd> -gpu host \
-vulkan-validation print \
-vulkan-validation-include-filter "ANGLE,com.example.game" 2>&1 | grep VVL
# Validate everything except the framework, and fail the offending call on error.
emulator @<avd> -gpu host \
-vulkan-validation fail \
-vulkan-validation-exclude-filter "android framework,system_ui"
Browse the source that implements each stage:
# Backend selection
grep -n "emuglConfig_get_renderer\|switchToSoftwareGles\|ANDROID_EGL_ON_EGL" \
external/qemu/android/android-ui/modules/aemu-gl-init/src/android/opengl/emugl_config.cpp
# The decode loop
grep -n "m_vkDec.decode\|m_glDec.decode\|m_gl2Dec.decode\|m_rcDec.decode" \
hardware/google/gfxstream/host/render_thread.cpp
# Vulkan loader basenames and physical-device scoring
grep -n "getPossibleLoaderPathBasenames\|deviceTypeScoreTable\|useMoltenVK" \
hardware/google/gfxstream/host/vulkan/*.cpp
Summary¶
- The host renderer is gfxstream's host library (formerly
libOpenglRender), loaded by the emulator UI through theRenderer/RenderLibinterface;RendererImplowns it, and the singletonFrameBufferis its global display state. - The
-gpuflag is resolved inemugl_config.cppinto five internal modes —host,swiftshader,swangle,lavapipe, anderror— withautochoosinghostwhen a usable GPU exists and software otherwise; platform fixups then constrain the choice per OS. - Each guest graphics context becomes a
RenderThreadwhose loop offers each command buffer to four decoders in turn (Vulkan, GLESv1, GLESv2/v3, renderControl), consuming whatever each recognizes; a read/write lock serializes GLES dispatch against EGL context changes to dodge a driver crash. - GLES is handled by a translator that implements GLES on top of either native desktop GL (GLX/WGL/CGL) or a host
libEGL(the EGL-on-EGL path, set byANDROID_EGL_ON_EGL); the same translator therefore drives desktop GL, ANGLE, and SwiftShader. - ANGLE translates GLES to Vulkan/D3D/Metal/desktop-GL;
swanglemeans ANGLE over SwiftShader's Vulkan; SwiftShader and mesa lavapipe/llvmpipe are the CPU-only fallbacks; virglrenderer is the alternative virtio-gpu renderer that Android does not use. rcGetGLStringsynthesizes the GL vendor/renderer/version and extension strings the guest sees, augmenting the host's real strings with gfxstream's pipe extensions and gating GPU-only extensions onSELECTED_RENDERER_HOST.- Vulkan is forwarded, not translated: the decoder remaps handles and memory and calls a real host driver via a dlopen'd loader; physical-device scoring prefers discrete GPUs; on macOS MoltenVK (a Metal-backed portability ICD) is selected via
ANDROID_EMU_VK_ICD=moltenvk. - The Khronos validation layers are loaded process-wide through
VK_INSTANCE_LAYERS, but listening to them is decided per guest application:-vulkan-validationpicks a behavior (off/print/fail/crash) and the include/exclude filters match the guest'sVkApplicationInfoapp and engine names exactly and case-insensitively, so only matching instances get aVK_EXT_debug_utilsmessenger attached. Enforcement applies to errors only —failreturnsVK_TRUEfrom the callback so the layer fails the call withVK_ERROR_VALIDATION_FAILED_EXT, whilecrashaborts. - Rendered pixels live in
ColorBufferobjects that may have GL and/or Vulkan backings with explicit flush/invalidate between them; aPostWorkerthread blits/presents the final buffer into the on-screen sub-window, while per-frame pixel readback is handled byReadbackWorker::doNextReadback()called on the render thread insidepostImpl(), delivering RGBA bytes to anOnPostCallbackfor recording and WebRTC.
Key Source Files¶
| File | Purpose |
|---|---|
hardware/google/gfxstream/host/frame_buffer.h |
Renderer global state; color-buffer registry, post, readback, display API |
hardware/google/gfxstream/host/renderer_impl.h |
Renderer implementation; owns render window, channels, threads |
hardware/google/gfxstream/host/render_thread.cpp |
Per-context decode loop dispatching to the four decoders |
external/qemu/android/android-ui/modules/aemu-gl-init/src/android/opengl/emugl_config.cpp |
-gpu mode resolution and backend/env wiring |
hardware/google/gfxstream/host/gl/glestranslator/egl/egl_global_info.cpp |
Native-GL vs. EGL-on-EGL backend selection in the GLES translator |
hardware/google/gfxstream/host/gl/glestranslator/egl/egl_os_api_egl.cpp |
EGL-on-EGL display, including ANGLE platform requests |
hardware/google/gfxstream/host/gl/emulation_gl.cpp |
GL emulation init, version negotiation, eglUseOsEglApi |
hardware/google/gfxstream/host/render_control.cpp |
renderControl handlers; rcGetGLString GL-string synthesis |
hardware/google/gfxstream/host/vulkan/vk_common_operations.cpp |
Vulkan instance/device creation, device scoring, MoltenVK selection |
hardware/google/gfxstream/host/vulkan/vulkan_dispatch.cpp |
Host Vulkan loader discovery, dispatch table population, validation-layer env setup |
hardware/google/gfxstream/host/vulkan/vk_vvl_configuration.cpp |
Validation behavior/filter parsing, per-app matching, debug-utils callback |
hardware/google/gfxstream/host/vulkan/vk_decoder_global_state.cpp |
Guest instance creation; attaches the debug messenger for matching apps |
hardware/google/gfxstream/host/color_buffer.h |
ColorBuffer GL/Vulkan dual backing and flush/invalidate |
hardware/google/gfxstream/host/post_worker_gl.cpp |
Posting a color buffer to the GL display/sub-window |
hardware/google/gfxstream/host/include/render-utils/Renderer.h |
OnPostCallback / readback callback signatures |