Skip to content

Chapter 3: Running the Emulator

Typing emulator @Pixel_8 looks like launching a single program, but the binary you invoke does almost no emulation. It is a thin launcher whose job is to figure out which architecture-specific QEMU binary to run, set up the library search path so the bundled graphics and Qt libraries are found, and then execv() into the real engine. The real engine, in turn, parses dozens of command-line options, reconciles them against the Android Virtual Device (AVD) on disk, materializes a hardware-qemu.ini that captures the final hardware configuration, and only then hands a translated argument vector to qemu_main().

This chapter walks the full path from process spawn to boot completed. We follow the launcher in android/emulator/main-emulator.cpp, the AVD discovery code in android/emu/avd/, the X-macro command-line option system in android/emu/cmdline/, the option-to-hardware translation in the QEMU glue, and the small set of special options (-accel-check, -qemu, -gpu) that change the launch flow entirely. By the end you should be able to read the verbose log of any emulator launch and know which source function produced each line.


3.1 The Launcher and the Engine

The program installed as emulator (or emulator.exe) is not the emulator. Its source comment in android/emulator/main-emulator.cpp is blunt about it:

// Source: external/qemu/android/emulator/main-emulator.cpp
/* This is the source code to the tiny "emulator" launcher program
 * that is in charge of starting the target-specific emulator binary
 * for a given AVD, i.e. either 'emulator-arm' or 'emulator-x86'
 */

The launcher's main() does a handful of things and then disappears: it parses just enough of the command line to learn the AVD name and target architecture, locates the matching qemu-system-* binary inside the bundled qemu/<os>-<hostarch>/ directory, prepends the right directories to the dynamic library search path, and finally calls safe_execv() to replace itself with the engine. Because it uses execv rather than spawning a child, the engine inherits the launcher's process id on POSIX, which keeps the process tree flat.

3.1.1 Why a separate launcher exists

A single emulator entry point can drive ARM64, x86, and x86_64 guests, each of which is a different QEMU binary compiled for a different target. The launcher decides which one to run from the AVD's CPU architecture rather than forcing the user to know it. It also centralizes host-environment fixups that must happen before the engine's shared libraries load: forcing LC_ALL=C to dodge locale-dependent parsing bugs, setting MESA_RGB_VISUAL, and on Linux pointing XDG_RUNTIME_DIR at a writable directory so lavapipe can create the temp files it needs for memory-fd export.

// Source: external/qemu/android/emulator/main-emulator.cpp
System::get()->envSet("LC_ALL", "C");
System::get()->envSet("MESA_RGB_VISUAL", "TrueColor 24");

3.1.2 Selecting the engine binary

getQemuExecutablePath() turns the AVD architecture into a QEMU architecture and builds a path. The mapping lives in getQemuArch(): on an x86_64 host, x86 maps to i386 and x86_64 maps to x86_64; on an aarch64 host, arm64 maps to aarch64. The final path follows the pattern <progDir>/qemu/<os>-<hostArch>/qemu-system-<qemuArch> — with a -headless suffix when the engine should run without a window.

// Source: external/qemu/android/emulator/main-emulator.cpp
#define QEMU_BINARY_PATTERN_HEADLESS "qemu-system-%s-headless%s"
#define QEMU_BINARY_PATTERN "qemu-system-%s%s"

Only the "ranchu" (QEMU2) virtual board is supported; isCpuArchSupportedByRanchu() accepts arm64, x86, and x86_64, and the launcher panics for anything else. The classic engine path (getClassicEmulatorPath()) is still present but deprecated, and -engine classic prints a warning.

The launcher tries several candidate directories for the binary — the program directory reported by the runtime, a sibling emulator/ directory derived from it, and the directory of argv[0] — because in platform builds the reported program directory is sometimes wrong (the code references bug 65257562 for this).

Launcher to engine handoff

flowchart TD
    USER["emulator @Pixel_8"] --> LAUNCH["emulator launcher<br/>main-emulator.cpp"]
    LAUNCH --> PARSE["scan argv for -avd / @name,<br/>-accel-check, -qemu, -gpu"]
    PARSE --> ARCH["path_getAvdTargetArch:<br/>read hw.cpu.arch"]
    ARCH --> QPATH["getQemuExecutablePath:<br/>qemu-system-{arch}"]
    QPATH --> LIBS["updateLibrarySearchPath<br/>lib64, gles, vulkan, qt"]
    LIBS --> EXEC["safe_execv into engine"]
    EXEC --> GLUE["qemu glue main<br/>android-qemu2-glue/main.cpp"]

3.1.3 Setting up the library search path

updateLibrarySearchPath() prepends <launcherDir>/lib64 and then a renderer-specific subdirectory (gles_angle or gles_swiftshader) plus a vulkan directory for ICDs. Which GLES directory wins depends on the -gpu value the launcher already scraped: -gpu lavapipe or any mode whose name contains angle forces the software ANGLE path. When the window is shown, androidQtSetupEnv() additionally adds the bundled Qt directory. These environment changes survive the execv, so the engine loads the bundled libraries instead of any system copies.

3.2 What an AVD Is on Disk

An AVD is two pieces of state: a small root ini and a content directory. The root ini lives at ~/.android/avd/<name>.ini and contains essentially one useful thing — a pointer to the content directory. The content directory holds config.ini (the hardware description), the disk images, snapshots, and runtime artifacts. The keys are defined in android/emu/avd/include/android/avd/keys.h:

// Source: external/qemu/android/emu/avd/include/android/avd/keys.h
#define  ROOT_ABS_PATH_KEY    "path"
#define  ROOT_REL_PATH_KEY    "path.rel"
#define CPU_ARCH "hw.cpu.arch"
#define  SEARCH_PREFIX   "image.sysdir."

3.2.1 Two ini files, two jobs

config.ini is device configuration: CPU architecture, RAM size, screen geometry, which sensors and cameras exist, which skin to load, and the image.sysdir.N keys that point at the system image. The engine reads it as the source of truth for hardware. The launcher reads a couple of keys from it directly to bootstrap — most importantly hw.cpu.arch, via _getAvdConfigValue():

// Source: external/qemu/android/emu/avd/src/android/avd/util.c
char* path_getAvdTargetArch( const char* avdName )
{
    char*  avdPath = path_getAvdContentPath(avdName);
    char*  avdArch = _getAvdConfigValue(avdPath, "hw.cpu.arch", "arm");
    ...
}

hardware-qemu.ini is different: it is generated output, not user input. The engine writes it once per launch after it has merged the command line, the skin's hardware.ini, and config.ini into a single final AndroidHwConfig. We return to this in section 3.6. It is one of the files -wipe-data deletes (see the clean_up_avd_contents_except_config_ini() list in the launcher), precisely because it is derived state.

3.2.2 Resolving the content directory

path_getAvdContentPath() reads the root ini and prefers the relative path key. It joins path.rel to the AVD home directory's parent and checks that a config.ini exists there; only if that fails does it fall back to the absolute path key. The relative-path-first policy lets an .android directory be copied between machines or home directories without rewriting absolute paths.

// Source: external/qemu/android/emu/avd/src/android/avd/util.c
const char* relPath = iniFile_getString(ini, ROOT_REL_PATH_KEY, NULL);
if (relPath != NULL) {
    p = bufprint_avd_home_path(temp, end);
    p = bufprint(p, end, PATH_SEP ".." PATH_SEP "%s", relPath);
    if (p < end && path_is_dir(temp)) { ... }
}

3.2.3 Finding the system image

The system image is not stored in the content directory; config.ini only records where to search for it with the image.sysdir.1 and image.sysdir.2 keys (at most MAX_SEARCH_PATHS, which is 2). path_getAvdSystemPath() reads each key, prefixes it with the SDK root when it is relative, and returns the first existing directory. The launcher uses this to sanity-check that a valid system path exists before it bothers launching the engine, panicking with a hint to set ANDROID_SDK_ROOT when it cannot.

AVD on-disk layout and resolution

flowchart TD
    ROOT["~/.android/avd/Pixel_8.ini<br/>root ini"] -->|"path.rel / path"| CONTENT["content dir<br/>Pixel_8.avd/"]
    CONTENT --> CFG["config.ini<br/>device config (input)"]
    CONTENT --> HWQ["hardware-qemu.ini<br/>generated (output)"]
    CONTENT --> IMG["userdata-qemu.img,<br/>snapshots/, sdcard.img"]
    CFG -->|"image.sysdir.1/2"| SYS["system image dir<br/>under SDK root"]
    CFG -->|"hw.cpu.arch"| ARCH["target arch -> engine binary"]

3.3 Discovering AVDs

-list-avds does not consult a registry; it scans the filesystem. avdScanner_new() opens the AVD home directory (~/.android/avd by default, or <sdk_home>/avd) and avdScanner_next() walks every directory entry, treating any name ending in .ini as an AVD and returning the name with the suffix stripped:

// Source: external/qemu/android/emu/avd/src/android/avd/scanner.c
if (entry_len < 4 ||
    memcmp(entry + entry_len - 4U, ".ini", 4) != 0) {
    // Can't possibly be a <name>.ini file.
    continue;
}
entry_len -= 4U;

The launcher drives this loop directly for -list-avds and -snapshot-list, printing one name per line and exiting without ever loading the engine. When list_snapshots is set, append_snapshot_names() additionally opens each AVD's snapshots/ directory and appends the snapshot names after a tab.

3.3.1 The search-directory precedence

When you name an AVD that does not resolve, the launcher prints the search order so you can debug it. The lookup honors three environment variables in order — ANDROID_AVD_HOME, then ANDROID_SDK_HOME (using its avd subdirectory), then $HOME/.android/avd. The error message in main-emulator.cpp spells this out verbatim, which is the canonical reference when an AVD "exists" but the emulator cannot find it.

3.4 The Command-Line Option System

The emulator has well over a hundred options. Rather than a giant if/strcmp ladder, they are declared once in an X-macro header, android/emu/cmdline/include/android/cmdline-options.h, and that single file is #included in several contexts with different macro definitions. Each option is one of three kinds:

  • OPT_FLAG(name, descr) — a boolean flag backed by an int
  • OPT_PARAM(name, template, descr) — a string-valued option backed by a char*
  • OPT_LIST(name, template, descr) — a repeatable option backed by a ParamList* linked list

A CFG_* variant marks options that describe AVD configuration and are ignored when -avd is given (they only matter when creating an AVD or running without one).

3.4.1 The struct and the table from one header

cmdline-definitions.h includes the option header to define the fields of the AndroidOptions struct:

// Source: external/qemu/android/emu/cmdline/include/android/cmdline-definitions.h
typedef struct AndroidOptions {
#define OPT_LIST(n, t, d) ParamList* n;
#define OPT_PARAM(n, t, d) char* n;
#define OPT_FLAG(n, d) int n;
#include "android/cmdline-options.h"
} AndroidOptions;

The parser, cmdline-option.cpp, includes the same header to build a parallel table of {name, struct-offset, type} records, so the field list and the parse table can never drift apart:

// Source: external/qemu/android/emu/cmdline/src/android/cmdline-option.cpp
#define  OPTION(_name,_type,_config)  \
    { #_name, offsetof(AndroidOptions,_name), _type, _config },
static const OptionInfo  option_keys[] = {
#define  OPT_FLAG(_name,_descr)             OPTION(_name,OPTION_IS_FLAG,0)
#define  OPT_PARAM(_name,_template,_descr)  OPTION(_name,OPTION_IS_PARAM,0)
#include "android/cmdline-options.h"
    { NULL, 0, 0, 0 }
};

3.4.2 How parsing actually works

android_parse_options() walks argv from the front. It special-cases @name as shorthand for -avd name, stops at the first argument that is not an option, translates dashes in the option name to underscores (so -no-window matches the field no_window), and looks the translated name up in option_keys. For a flag it writes 1 to the int field at the recorded offset; for a param it strdups the next argument into the char* field; for a list it pushes onto a linked list (later reversed so order is preserved). Anything it does not recognize stops the loop and is left in argv for downstream handling — including everything after -qemu.

X-macro option flow

flowchart LR
    HDR["cmdline-options.h<br/>OPT_FLAG / OPT_PARAM / OPT_LIST"]
    HDR -->|"define fields"| STRUCT["AndroidOptions struct"]
    HDR -->|"build name+offset table"| TABLE["option_keys[]"]
    ARGV["argv tokens"] --> PARSE["android_parse_options"]
    TABLE --> PARSE
    PARSE -->|"offsetof writes"| STRUCT
    STRUCT --> ENGINE["engine reads opts->field"]

3.4.3 Debug tags

-debug <tags> and -debug-<tag> are handled before the table lookup. android_parse_debug_tags_option() parses a comma-separated list where a - or no- prefix disables a tag, applying them left to right so the last value wins. -verbose is kept for backward compatibility and is rewritten internally to -debug-init.

3.5 The Engine's main(): From Options to QEMU

When the engine binary starts, control reaches main() in android-qemu2-glue/main.cpp. After early setup and crash-handler init, it kicks off an asynchronous host-GPU query, injects the console agents that give the rest of the code access to global state, records the original command line for diagnostics, and then calls the big workhorse:

// Source: external/qemu/android-qemu2-glue/main.cpp
AndroidOptions* opts = &sOpts[0];
AndroidHwConfig* hw = getConsoleAgents()->settings->hw();
if (!emulator_parseCommonCommandLineOptions(&argc, &argv,
                                            kTarget.androidArch,
                                            true,  // is_qemu2
                                            opts, hw, &avd, &exitStatus)) {
    ...
}

emulator_parseCommonCommandLineOptions() (in android/android-emu/android/main-common.c) first calls android_parse_options() to fill opts, reconfigures logging from the parsed flags, injects the options into the global console-agent state, and then scans the remaining arguments for -qemu.

3.5.1 The -qemu passthrough boundary

-qemu is the escape hatch: everything after it is meant for QEMU directly, untranslated. The common parser breaks its scan loop as soon as it sees the token:

// Source: external/qemu/android/android-emu/android/main-common.c
if (!strcmp(opt, "-qemu")) {
    --(*p_argc);
    ++(*p_argv);
    break;
}

Two paths reach qemu_main() without the normal AVD-driven translation. The launcher itself short-circuits when it sees a leading -qemu (or -fuchsia) and sets forceEngineLaunch, letting the engine boot without an AVD. Inside the engine, when emulator_parseCommonCommandLineOptions() returns false with exitStatus == EMULATOR_EXIT_STATUS_POSITIONAL_QEMU_PARAMETER (defined as -1 in main-common.h), the glue copies the remaining positional arguments straight into the QEMU argument vector and jumps to enter_qemu_main_loop(), skipping option translation entirely. The Fuchsia branch does the same with a few extra -kernel/-L arguments and feature-flag defaults.

3.5.2 Building the AVD and the hardware config

In the normal path, createAVD() builds an AvdInfo by calling avdInfo_new(opts->avd, ...), which loads the root ini and config.ini from disk. avdInfo_new() reads the content directory, the API level, the system-image search paths, and the config.ini itself into the AvdInfo. The merge into the final hardware config happens in avdInfo_initHwConfig():

// Source: external/qemu/android/emu/avd/src/android/avd/info.c
androidHwConfig_init(hw, i->apiLevel);          // defaults first
if (i->skinHardwareIni != NULL)
    ret = androidHwConfig_read(hw, i->skinHardwareIni);   // skin overrides defaults
if (ret == 0 && i->configIni != NULL)
    ret = androidHwConfig_read(hw, i->configIni);         // config.ini overrides skin

The precedence is defaults, then the skin's hardware.ini, then the device's config.ini — the comment in the source notes that config.ini overriding the skin "is preferable to the opposite order." Command-line options that map to hardware fields are applied on top of this by the glue before the final ini is written.

Engine launch sequence

sequenceDiagram
    participant Glue as qemu glue main
    participant Common as emulator_parseCommonCommandLineOptions
    participant Avd as AvdInfo
    participant Hw as AndroidHwConfig
    participant Qemu as qemu_main
    Glue->>Common: parse argc/argv
    Common->>Common: android_parse_options fills opts
    Common->>Common: scan for -qemu boundary
    Common->>Avd: createAVD / avdInfo_new
    Avd->>Hw: avdInfo_initHwConfig merge
    Glue->>Hw: apply -gpu, -memory, -cores, etc
    Glue->>Glue: genHwIniFile writes hardware-qemu.ini
    Glue->>Qemu: enter_qemu_main_loop with -android-hw path

3.6 Generating hardware-qemu.ini

Once the merged AndroidHwConfig is final, the glue serializes it to disk and tells QEMU where to find it. genHwIniFile() writes a clean copy (dropping defaulted entries so it can be compared against a snapshot's recorded config), and the path is passed as -android-hw:

// Source: external/qemu/android-qemu2-glue/main.cpp
int ret = genHwIniFile(hw, coreHwIniPath);
if (ret != 0) return ret;
args.add2("-android-hw", coreHwIniPath);

The AndroidHwConfig struct is itself X-macro generated. hardware-properties.ini is the master description of every hardware property — its name, type, default, and documentation — and android/scripts/gen-hw-config.py turns it into hw-config-defs.h. That generated header defines the struct fields, the loader, and the writer all from one list:

// Source: external/qemu/objs/avd_config/android/avd/hw-config-defs.h
HWCFG_STRING(
  hw_cpu_arch,
  "hw.cpu.arch",
  "arm",
  ...

So a property added to hardware-properties.ini automatically becomes a config.ini key the loader understands, a field in AndroidHwConfig, and a line the writer emits into hardware-qemu.ini — no hand-written plumbing. This hardware-qemu.ini is the contract between the android-emu side and the QEMU machine model: when QEMU starts, it reads it back via -android-hw to learn how many cores, how much RAM, which serial-port naming scheme, and which virtual devices to instantiate.

3.7 Acceleration, GPU Modes, and -accel-check

Three options change how the guest CPU and GPU are virtualized, and one of them never launches the engine at all.

3.7.1 -accel-check and emulator-check

-accel-check does not run a VM; it answers a question Android Studio asks before offering to start one. The launcher intercepts the flag and forwards it to a separate bundled executable, emulator-check, with the argument accel:

// Source: external/qemu/android/emulator/main-emulator.cpp
const auto path = sys.findBundledExecutable("emulator-check");
...
bool ret = sys.runCommand(
        {path, "accel"},
        RunOptions::WaitForCompletion | RunOptions::ShowOutput,
        System::kInfinite, &exit_code);

emulator-check (android/emulator-check/main-emulator-check.cpp) is a tiny program with a table of subcommands — accel, cpu-info, window-mgr, desktop-env, and on Windows hyper-v/whpx. The accel handler calls androidCpuAcceleration_getStatus() and returns its numeric status plus a human message. The status codes are a stable contract: cpu_accelerator.h declares the enum with the comment "don't change these numbers / Android Studio depends on them," where 0 means ANDROID_CPU_ACCELERATION_READY and non-zero values encode specific failures (no VT-x, /dev/kvm missing, permission denied, Hyper-V conflict, and so on).

3.7.2 Choosing the accelerator

In the normal launch path, handleCpuAcceleration() maps the -accel on|off|auto option (and the -no-accel shorthand) onto a mode, then queries androidCpuAcceleration_getStatus(). The chosen accelerator becomes a QEMU -enable-* flag through getAcceleratorEnableParam():

// Source: external/qemu/android/android-emu/android/main-common.c
case ANDROID_CPU_ACCELERATOR_KVM:  return "-enable-kvm";
case ANDROID_CPU_ACCELERATOR_HVF:  return "-enable-hvf";
case ANDROID_CPU_ACCELERATOR_WHPX: return "-enable-whpx";
case ANDROID_CPU_ACCELERATOR_AEHD: return "-enable-aehd";

Because x86/x86_64 guests run unacceptably slowly under pure TCG translation, the code refuses to start an x86 guest in auto mode when no hardware accelerator is available and prints a link to the acceleration setup docs. On Apple silicon the same guard applies to arm64 guests and prefers Hypervisor.framework (HVF). The accelerators themselves are the subject of a later chapter; here the point is that the choice is made during launch and emitted as a QEMU flag.

3.7.3 GPU modes

The -gpu <mode> option (declared OPT_PARAM(gpu, ...)) selects the graphics backend. emuglConfig_get_renderer() in emugl_config.cpp maps the user string to a renderer enum:

// Source: external/qemu/android/android-ui/modules/aemu-gl-init/src/android/opengl/emugl_config.cpp
} 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;
}

host uses the machine's real GPU; swiftshader and swangle are CPU software rasterizers; lavapipe is a software Vulkan implementation. The full list the config accepts also includes auto, which probes the host GPU (the asynchronous query started at the top of main()) and picks host when a usable GPU is found, falling back to software otherwise. The launcher reads -gpu early because it affects the library search path — lavapipe and angle modes force the software-ANGLE library directory before the engine loads, as we saw in section 3.1.3.

GPU mode resolution

flowchart TD
    OPT["-gpu mode"] --> CHECK{"which mode"}
    CHECK -->|"auto"| PROBE["async host GPU query"]
    PROBE -->|"GPU usable"| HOST["SELECTED_RENDERER_HOST"]
    PROBE -->|"no usable GPU"| SW["software renderer"]
    CHECK -->|"host / on"| HOST
    CHECK -->|"swiftshader"| SWS["SWIFTSHADER_INDIRECT"]
    CHECK -->|"swangle"| ANG["ANGLE_INDIRECT"]
    CHECK -->|"lavapipe / llvmpipe"| LVP["LAVAPIPE (software Vulkan)"]

3.8 The Lifecycle: Launch to Boot Complete

With options parsed, the AVD merged, hardware-qemu.ini written, and accelerator/GPU chosen, the glue builds the final QEMU argument vector and spawns the VM. skin_winsys_spawn_thread() runs enter_qemu_main_loop() on a dedicated thread (or directly when -no-window is set), which calls run_qemu_main() — the QEMU machine setup that creates the CPUs, RAM, and virtual devices described by the hardware ini.

3.8.1 Window vs. headless

The relationship between -no-window and the headless binary is subtle. The launcher treats both -no-window and -no-qt as a request for a headless launch and selects the qemu-system-*-headless binary. Inside the engine, emulator_parseCommonCommandLineOptions() then forces opts->no_window = false, because (per the in-source comment referencing bug 143949261) windowlessness is now an inherent property of which binary was selected, not a runtime flag. So the launcher's binary choice, not the option value the engine sees, is what determines whether a UI thread is created.

3.8.2 Signaling boot completion

The guest tells the host it has finished booting over a QEMU pipe named QemuMiscPipe. When userspace writes the message bootcomplete to that pipe, qemuMiscPipeDecodeAndExecute() dispatches it:

// Source: external/qemu/android/android-emu/android/emulation/QemuMiscPipe.cpp
} else if (beginWith(input, "bootcomplete")) {
    fillWithOK(output);
    std::thread{bootCompleteFunction}.detach();
    return;
}

bootCompleteFunction() computes the boot time, reports it as a metric, touches bootcompleted.ini in the content directory, and flips the global flag through set_guest_boot_completed(true). That bootcompleted.ini file is why the launcher deletes it at the start of every run: a stale copy from a previous boot must not be mistaken for the current one. If -quit-after-boot (the test_quitAfterBootTimeOut field) was set, the same function shuts the VM down immediately after boot completes — the mechanism behind smoke-test invocations.

Boot completion signaling

sequenceDiagram
    participant Guest as Android guest userspace
    participant Pipe as QemuMiscPipe service
    participant Misc as bootCompleteFunction
    participant Glob as console-agent globals
    Guest->>Pipe: write "bootcomplete"
    Pipe->>Pipe: qemuMiscPipeDecodeAndExecute
    Pipe->>Misc: spawn detached thread
    Misc->>Misc: touch bootcompleted.ini
    Misc->>Glob: set_guest_boot_completed(true)
    Misc->>Misc: report boot duration metric

3.8.3 Restart and clean shutdown

The launcher captures restart parameters with initializeEmulatorRestartParameters() before it mangles argv, so the engine can relaunch itself with the same options after a quickboot restart. -read-only disables restart (and snapshot saving) so multiple instances can share one AVD. The -is-restart <pid> option, set when the engine respawns, makes the new launcher wait up to ten seconds for the old process to exit before proceeding. The launcher's small -kill/-sleep handler (run before anything else in main()) is the watchdog used to terminate a hung emulator process by pid.

3.9 One Engine, Many Form Factors

There is no separate "Wear emulator" or "Android TV emulator" binary. The same qemu-system-* engine and the same launcher you have followed through this chapter boot a watch, a television, a foldable phone, a car head unit, a desktop, and an XR headset. What differs is entirely data: the AVD's config.ini, the system image's build.prop, and the chosen skin. Three pieces of derived state turn that data into device-specific behavior — an AvdFlavor classification, a hardware profile of geometry and input devices, and per-device sensor configuration — and all three flow through the same avdInfo_initHwConfig merge (section 3.6) into one hardware-qemu.ini.

Diagram: one AVD configuration, many form factors

flowchart TD
    BP["build.prop<br/>ro.product.* names"] --> FL["propertyFile_getAvdFlavor<br/>→ AvdFlavor"]
    CFG["config.ini<br/>hw.device.name, hw.lcd.*,<br/>hw.sensor.hinge.*, skin.name"] --> MERGE["avdInfo_initHwConfig<br/>defaults + skin + config.ini"]
    FL --> AVD["AvdInfo<br/>(flavor + hw config)"]
    MERGE --> AVD
    AVD --> HW["hardware-qemu.ini<br/>handed to QEMU"]
    AVD -->|"flavor + hinge config"| B1["Sensors: hinge, posture,<br/>heart rate (Ch 10)"]
    AVD -->|"flavor + lcd config"| B2["Display: round face, multi-display,<br/>stacked car layout (Ch 17)"]
    AVD -->|"flavor"| B3["UI: which extended-control<br/>panels appear (Ch 22)"]

3.9.1 The AVD flavor

The coarse device class is an AvdFlavor enum, declared in external/qemu/android/emu/avd/include/android/avd/util.h:125:

// Source: external/qemu/android/emu/avd/include/android/avd/util.h
typedef enum {
    AVD_PHONE = 0,
    AVD_TV = 1,
    AVD_WEAR = 2,
    AVD_ANDROID_AUTO = 3,
    AVD_DESKTOP = 4,
    AVD_XR = 5,
    AVD_GLASSES = 6,
    AVD_OTHER = 255,
} AvdFlavor;

The flavor is not stored in config.ini; it is recovered from the system image's build properties. propertyFile_getAvdFlavor (external/qemu/android/emu/avd/src/android/avd/util.c:236) matches the product name against a small table of substrings and returns the first hit:

// Source: external/qemu/android/emu/avd/src/android/avd/util.c
const char* phone_names[]   = {"phone"};
const char* tv_names[]      = {"atv"};
const char* wear_names[]    = {"aw", "wear"};
const char* car_names[]     = {"car"};
const char* desktop_names[] = {"pc", "desktop"};
const char* xr_names[]      = {"xr"};
const char* glasses_names[] = {"glasses"};

avdInfo_initHwConfig stores the result on the AvdInfo (external/qemu/android/emu/avd/src/android/avd/info.c:889), and every form-factor decision downstream reads it back through avdInfo_getAvdFlavor (external/qemu/android/emu/avd/include/android/avd/info.h:214). A desktop image additionally gates some behavior on API level via avdInfo_isDesktopApi36OrHigher (info.c:573).

3.9.2 The device profile: geometry, density, and input

Within a flavor, the concrete device is named by hw.device.name (a profile id such as pixel_6, wearos_small_round, or tv_1080p). The profile supplies the screen and input keys the SDK writes into config.ini, all declared in the host-common config schema (hardware/google/aemu/host-common/include/host-common/hw-config-defs.h):

  • hw.device.name (:892) — the profile id, read back wherever a feature needs the exact device (for example the Pixel Fold check below).
  • hw.lcd.width / hw.lcd.height / hw.lcd.density / hw.lcd.depth — the panel geometry that becomes the guest framebuffer.
  • hw.lcd.circular (:283) — a round display, the tell-tale of a circular Wear OS watch face.
  • hw.keyboard (:108) and hw.rotaryInput (:136) — input hardware; the rotary input models a Wear OS bezel/crown.

Skins layer a bezel and a set of orientation layouts on top of that geometry. The skin keys live in external/qemu/android/emu/avd/include/android/avd/keys.h:

// Source: external/qemu/android/emu/avd/include/android/avd/keys.h
#define  SKIN_PATH       "skin.path"
#define  SKIN_NAME       "skin.name"
#define  PIXEL_FOLD_DEFAULT_SKIN_NAME       "default"
#define  PIXEL_FOLD_CLOSED_SKIN_NAME        "closed"
#define  SKIN_DEFAULT    "HVGA"

A phone needs one skin; a foldable needs two (the open "default" and the folded "closed"), which is why the Pixel Fold has dedicated skin-name constants. The skin's own hardware.ini participates in the merge from section 3.6, sitting between the built-in defaults and config.ini.

3.9.3 Foldables, rollables, and the resizable AVD

Folding and rolling devices add a block of hw.sensor.* keys that configure the FoldableModel covered in Chapter 10. The schema (hw-config-defs.h) declares hw.sensor.hinge (:710), hw.sensor.hinge.count (:717), hw.sensor.hinge.ranges, hw.sensor.posture_list, and the rollable equivalents under hw.sensor.roll. Setting a hinge angle through the control plane recomputes a discrete posture (POSTURE_CLOSED, POSTURE_HALF_OPENED, POSTURE_OPENED, POSTURE_FLIPPED, POSTURE_TENT) — the state machine in section 10.7 — and the display side (section 17.10) lights the inner or outer panel accordingly.

A Pixel-class fold is special-cased. android_foldable_is_pixel_fold (external/qemu/android/android-emu/android/hw-sensors.cpp:1438) returns true when the device name contains fold and the SupportPixelFold feature is on, or whenever the resizable-34 configuration is active:

// Source: external/qemu/android/android-emu/android/hw-sensors.cpp
bool android_foldable_is_pixel_fold() {
    if (resizableEnabled34()) {
        return true;
    }
    const auto devname = getConsoleAgents()->settings->hw()->hw_device_name;
    return (devname && std::string::npos != std::string(devname).find("fold") &&
            fc::isEnabled(fc::SupportPixelFold));
}

The resizable AVD is one device that morphs between form factors at runtime instead of fixing one at create time. Its presets are an enum in external/qemu/android/android-emu/android/emulation/resizable_display_config.h:20:

// Source: external/qemu/android/android-emu/android/emulation/resizable_display_config.h
enum PresetEmulatorSizeType {
    PRESET_SIZE_PHONE = 0,
    PRESET_SIZE_UNFOLDED = 1,
    PRESET_SIZE_TABLET = 2,
    PRESET_SIZE_MAX = 3,
};

The three sizes (geometry plus density) come from a single hw.resizable.configs string, whose built-in default is parsed in resizable_display_config.cpp:47:

// Source: external/qemu/android/android-emu/android/emulation/resizable_display_config.cpp
"phone-0-1080-2340-420, unfolded-1-1768-2208-420,"
"tablet-2-1920-1200-240";

Each entry is name-id-width-height-dpi; switching presets reconfigures the panel without relaunching the engine, which is how a single resizable image previews phone, unfolded, and tablet layouts.

3.9.4 How the flavor steers the runtime

Once classified, the flavor reaches into both the display pipeline and the UI. On the display side, automotive devices get a multi-display stacked layout: getDisplayType (external/qemu/android/android-emu/android/emulation/AutoDisplays.cpp:33) maps the AVD to DISTANT_DISPLAY, DYNAMIC_MULTI_DISPLAY, or GENERIC_DISPLAY, and MultiDisplay::recomputeStackedLayoutLocked arranges the cluster and center-stack panels (section 17.8). The setMultiDisplay entry point refuses TV and Wear flavors outright (section 17.6).

On the UI side, the extended-controls window enables or hides whole panels by flavor. The multi-display panel, for instance, is gated so that TV, Wear, XR, and Glasses never see it (external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/extended-window.cpp:226):

// Source: aemu-ui-qt/src/android/skin/qt/extended-window.cpp
avdFlavor != AVD_TV &&
avdFlavor != AVD_WEAR && avdFlavor != AVD_XR && avdFlavor != AVD_GLASSES &&

Further down, the same constructor branches once per flavor to add device-appropriate controls: a TV remote page (:332), Wear-specific controls (:341), the car data pages (:348), and an XR/Glasses input mode (:376). The sensor layer makes the matching move at init time — a Wear OS image auto-enables the heart-rate and wrist-tilt sensors and an Automotive image enables the heading sensor, as Chapter 10 describes. The net effect is that one engine presents itself as whatever device the AVD's flavor, profile, and skin describe.

3.10 Try It

The following commands exercise the launch path described above. They assume a configured SDK with at least one AVD.

  • List the AVDs the scanner finds on disk, exactly as -list-avds walks ~/.android/avd:
emulator -list-avds
  • Check CPU acceleration without launching a VM. This invokes the bundled emulator-check accel and prints a status code (0 means ready) plus a message:
emulator -accel-check
  • Run the underlying check binary directly to see the other subcommands:
emulator-check accel
emulator-check cpu-info
  • Launch verbosely and watch the launcher resolve the engine binary, library paths, and final argument vector:
emulator @My_AVD -verbose -debug-init
  • Inspect an AVD's input config and the generated hardware config side by side (run once after a boot so hardware-qemu.ini exists):
cat ~/.android/avd/My_AVD.avd/config.ini
cat ~/.android/avd/My_AVD.avd/hardware-qemu.ini
  • Force a software renderer and a specific accelerator state, then pass a raw flag straight through to QEMU after the -qemu boundary:
emulator @My_AVD -gpu swiftshader -accel off -qemu -m 4096
  • Boot, then quit as soon as the guest signals completion over QemuMiscPipe (useful for scripted smoke tests):
emulator @My_AVD -no-window -quit-after-boot 120

Summary

  • The installed emulator program is a thin launcher in android/emulator/main-emulator.cpp; it picks the right qemu-system-* binary by AVD architecture, fixes up the library search path, and safe_execvs into the real engine.
  • An AVD is a root ini (~/.android/avd/<name>.ini) pointing at a content directory; config.ini is device configuration input while hardware-qemu.ini is generated output the engine recreates each launch.
  • -list-avds scans the filesystem via avdScanner_*, treating every *.ini in the AVD home as an AVD; the AVD home is resolved through ANDROID_AVD_HOME, ANDROID_SDK_HOME, then $HOME/.android/avd.
  • Command-line options are declared once in the cmdline-options.h X-macro header and reused to define the AndroidOptions struct and the parser's offset table, so the two can never drift.
  • -qemu is a hard boundary: everything after it bypasses option translation and goes straight to qemu_main(), signaled internally by EMULATOR_EXIT_STATUS_POSITIONAL_QEMU_PARAMETER.
  • The final hardware config is built by merging defaults, the skin's hardware.ini, and config.ini, then serialized by genHwIniFile() and handed to QEMU as -android-hw.
  • -accel-check forwards to the standalone emulator-check binary and returns stable status codes that Android Studio depends on; the normal path turns the accelerator choice into a QEMU -enable-* flag.
  • -gpu selects between the host GPU and software renderers (swiftshader, swangle, lavapipe); auto probes the host GPU asynchronously during main().
  • Boot completion is signaled by the guest writing bootcomplete to QemuMiscPipe, which runs bootCompleteFunction(), touches bootcompleted.ini, and flips the guest_boot_completed global.
  • One engine serves every form factor: an AvdFlavor recovered from build.prop (AVD_PHONE/AVD_TV/AVD_WEAR/AVD_ANDROID_AUTO/AVD_DESKTOP/AVD_XR/AVD_GLASSES), a hw.device.name profile of geometry and input, optional hw.sensor.hinge.* foldable config, and the resizable AVD's preset sizes all feed the same hardware-qemu.ini and then steer sensors (Ch 10), displays (Ch 17), and which UI panels appear (Ch 22).

Key Source Files

File Purpose
external/qemu/android/emulator/main-emulator.cpp Launcher: engine selection, library paths, -accel-check/-qemu/-gpu early handling, execv
external/qemu/android/emu/avd/src/android/avd/util.c AVD content-path and target-arch resolution; propertyFile_getAvdFlavor flavor detection
external/qemu/android/emu/avd/include/android/avd/util.h The AvdFlavor enum (phone, TV, Wear, Auto, desktop, XR, glasses)
hardware/google/aemu/host-common/include/host-common/hw-config-defs.h Schema for hw.device.name, hw.lcd.*, and the hw.sensor.hinge.* foldable keys
external/qemu/android/android-emu/android/emulation/resizable_display_config.cpp Resizable AVD presets and the hw.resizable.configs default string
external/qemu/android/emu/avd/src/android/avd/scanner.c Filesystem scan backing -list-avds and -snapshot-list
external/qemu/android/emu/avd/include/android/avd/keys.h Names of the config.ini and root-ini keys
external/qemu/android/emu/cmdline/include/android/cmdline-options.h The X-macro declaration of every command-line option
external/qemu/android/emu/cmdline/src/android/cmdline-option.cpp android_parse_options() and the offset-table parser
external/qemu/android-qemu2-glue/main.cpp Engine main(): option parsing, AVD build, genHwIniFile, QEMU launch
external/qemu/android/android-emu/android/main-common.c emulator_parseCommonCommandLineOptions, -qemu boundary, handleCpuAcceleration
external/qemu/android/emu/avd/src/android/avd/info.c avdInfo_initHwConfig merge of defaults, skin, and config.ini
external/qemu/android/emulator-check/main-emulator-check.cpp Standalone emulator-check answering accel, cpu-info, etc.
external/qemu/android/emu/feature/include/android/cpu_accelerator.h Stable AndroidCpuAcceleration status codes
external/qemu/android/android-ui/modules/aemu-gl-init/src/android/opengl/emugl_config.cpp -gpu mode to renderer mapping
external/qemu/android/android-emu/android/emulation/QemuMiscPipe.cpp bootcomplete handling and guest_boot_completed