Render on the GPU with no display, through Xwayland
This VM sees the host's 7900 XT through virtio-gpu and renderD128 is
readable without the video group, so the software rasteriser was never
necessary. What was in the way is that the emulator's `-gpu host` renderer
speaks GLX: with no DISPLAY it cannot open a context at all
("GlxEnginegetDefaultDisplay: Failed to open display 0") and silently falls
back.
So `emu up` now starts a headless sway whose only job is to provide an
Xwayland, and Xwayland's GLX runs on the render node. The guest's GLES
reports `virgl (AMD Radeon RX 7900 XT)` afterwards. One compositor per
machine, on a named socket so a second `emu up` reuses it; no seat, no DRM
master, no card node.
Scrolling a list in the stock Settings app: 25.5% janky frames and 8 slow
UI-thread frames on software, 3.3% and 0 on the GPU. An emulator also costs
1.1 GB less resident (2.9 GB against 3.9 GB), which is real headroom on a
machine that only takes two.
The reason this is worth the machinery is measurement rather than speed.
Before it, frame timings from in here said nothing about an app at all --
ai-app scrolled *better* than Settings, because both were bounded by the
rasteriser rather than by anything either was doing.
Host Vulkan is off in `GPU_HOST_FEATURES`, and that one is a bug rather than
a preference: gfxstream asks Venus for a memory type for its ColorBuffers
that Venus does not offer, and the emulator dies before adb sees it. GLES is
unaffected. Retry it whenever mesa moves; the failure is loud.
A missing sway, or one that will not start, says so and falls back to
software exactly as before -- verified by moving the config aside.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fe97956632
commit
1d71730b56
3 files changed
+137
-2
No files matched your search
@@ -40,6 +40,48 @@ about one device (`adb devices`) all turn the defaulting off, and nothing is
|
||||
guessed when this checkout's emulator is not running: adb's own error is
|
||||
better than a wrapper picking a stranger's device.
|
||||
|
||||
## Rendering on the GPU with no display
|
||||
|
||||
This machine has no screen, and for a long time that meant the emulator
|
||||
rasterised in software. It does not have to: the VM sees the host's GPU
|
||||
through virtio-gpu, and `/dev/dri/renderD128` is readable without being in
|
||||
the `video` group.
|
||||
|
||||
What was in the way is that the emulator's `-gpu host` renderer speaks GLX,
|
||||
so with no `DISPLAY` it cannot open a context at all -- it says
|
||||
`GlxEnginegetDefaultDisplay: Failed to open display 0` and falls back. So
|
||||
`emu up` starts a **headless sway** whose only job is to provide an
|
||||
Xwayland, and Xwayland's GLX runs on the render node. The guest's GLES then
|
||||
reports `virgl (AMD Radeon RX 7900 XT)` instead of a software rasteriser.
|
||||
One compositor is started for the machine and reused, on a named socket
|
||||
under `XDG_RUNTIME_DIR`; it needs no seat, no DRM master and no card node.
|
||||
|
||||
Measured 2026-08-30, scrolling a list in the stock **Settings** app:
|
||||
|
||||
| | janky frames | 90th percentile | slow UI-thread frames |
|
||||
|---|---|---|---|
|
||||
| software | 25.5% | 93ms | 8 |
|
||||
| on the GPU | 3.3% | 28ms | 0 |
|
||||
|
||||
An emulator also costs about **1.1 GB less** resident this way (2.9 GB
|
||||
against 3.9 GB), which is a whole emulator's worth of headroom on a machine
|
||||
that only takes two.
|
||||
|
||||
This matters for more than speed. Before it, frame timings measured in here
|
||||
said nothing at all about an app: our own app scrolled *better* than
|
||||
Settings did, because both were bounded by the rasteriser rather than by
|
||||
anything either of them was doing. Now the platform floor is low enough that
|
||||
an app's own jank is visible above it.
|
||||
|
||||
`GPU_HOST_FEATURES` carries the one workaround: host Vulkan is switched off,
|
||||
because gfxstream asks Venus for a memory type for its ColorBuffers that
|
||||
Venus does not offer, and the emulator dies before adb sees it. GLES is
|
||||
unaffected. It is worth retrying whenever mesa moves -- the failure is loud
|
||||
and costs one boot to find.
|
||||
|
||||
If sway is missing, or the compositor will not start, `emu up` says so and
|
||||
starts on software rendering as it always did. None of this is essential.
|
||||
|
||||
## Refusing to start one
|
||||
|
||||
`emu up` checks `MemAvailable` first and refuses if starting an emulator
|
||||
|
||||
@@ -112,6 +112,72 @@ check_room() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Host Vulkan is off, and this is the one flag here that is about a bug
|
||||
# rather than a preference.
|
||||
#
|
||||
# gfxstream asks the host for a memory type it can use for its ColorBuffers,
|
||||
# and Venus -- the virtio-gpu Vulkan driver this VM sees the real GPU
|
||||
# through -- does not offer one: "Format VK_FORMAT_R8G8B8A8_UNORM is not
|
||||
# supported", then "Failed to find memory type for ColorBuffers", then the
|
||||
# emulator dies before adb ever sees it. GLES on the same driver is fine, and
|
||||
# is what the guest falls back to, so the only thing lost is Vulkan inside
|
||||
# the guest. Try dropping this whenever mesa moves: the failure is loud and
|
||||
# immediate, so it costs one boot to find out.
|
||||
GPU_HOST_FEATURES=${GPU_HOST_FEATURES:--feature -Vulkan}
|
||||
|
||||
# An X server on this machine's GPU, started once and reused, or nothing.
|
||||
#
|
||||
# The emulator's `-gpu host` renderer talks GLX. With no DISPLAY it cannot
|
||||
# open one -- "GlxEnginegetDefaultDisplay: Failed to open display 0" -- and
|
||||
# the only other thing it will do is rasterise in software. Measured on this
|
||||
# VM on 2026-08-30, that is not a small difference: scrolling a list in the
|
||||
# stock Settings app went from 25% janky frames to 3%, and the emulator's own
|
||||
# resident size fell from 3.9 GB to 2.9 GB.
|
||||
#
|
||||
# So: a headless sway, purely to get an Xwayland whose GLX runs on the
|
||||
# virtio-gpu render node. It needs no seat, no DRM master and no card node --
|
||||
# only /dev/dri/renderD128, which is world-readable here. Writes nothing
|
||||
# outside XDG_RUNTIME_DIR and goes away with the machine.
|
||||
#
|
||||
# Prints the DISPLAY it ended up with; exit 1 means the caller should fall
|
||||
# back to software rather than fail, because none of this is essential.
|
||||
headless_display() {
|
||||
command -v sway >/dev/null 2>&1 || return 1
|
||||
[ -e /dev/dri/renderD128 ] || return 1
|
||||
|
||||
run="${XDG_RUNTIME_DIR:-/tmp}/emulator-tools"
|
||||
mkdir -p "$run"
|
||||
export SWAYSOCK="$run/sway.sock"
|
||||
# Named rather than left to sway's pid-based default, which is what makes
|
||||
# a second `emu up` reuse the first one's compositor instead of starting
|
||||
# another beside it.
|
||||
if ! swaymsg -t get_version >/dev/null 2>&1; then
|
||||
rm -f "$SWAYSOCK"
|
||||
conf="$(dirname "$(readlink -f "$0")")/../share/emu/sway.conf"
|
||||
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 LIBSEAT_BACKEND=noop \
|
||||
setsid sway -c "$conf" >"$run/sway.log" 2>&1 &
|
||||
for _ in $(seq 20); do
|
||||
swaymsg -t get_version >/dev/null 2>&1 && break
|
||||
sleep 0.5
|
||||
done
|
||||
swaymsg -t get_version >/dev/null 2>&1 || {
|
||||
echo "emu: could not start the headless compositor; see $run/sway.log" >&2
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
# Asked of the compositor rather than guessed, because Xwayland takes the
|
||||
# first free display number and this machine may already have one.
|
||||
rm -f "$run/display"
|
||||
swaymsg exec -- "sh -c 'printf %s \"\$DISPLAY\" > $run/display'" >/dev/null 2>&1
|
||||
for _ in $(seq 20); do
|
||||
[ -s "$run/display" ] && break
|
||||
sleep 0.5
|
||||
done
|
||||
[ -s "$run/display" ] || return 1
|
||||
cat "$run/display"
|
||||
}
|
||||
|
||||
cmd_up() {
|
||||
if serial=$(avd_serial "$adb" "$avd"); then
|
||||
echo "emu: '$avd' is already running ($serial)" >&2
|
||||
@@ -143,12 +209,16 @@ cmd_up() {
|
||||
pkill -f "[e]mulator.*-avd $avd" >/dev/null 2>&1 || true
|
||||
|
||||
log="/tmp/$avd-emulator.log"
|
||||
: >"$log"
|
||||
rm -f "$log"
|
||||
if [ -n "${DISPLAY:-}" ] || [ -n "${WAYLAND_DISPLAY:-}" ]; then
|
||||
echo "emu: starting '$avd' with GPU acceleration" >&2
|
||||
"$sdk/emulator/emulator" -avd "$avd" -gpu host -no-audio >"$log" 2>&1 &
|
||||
elif display=$(headless_display); then
|
||||
echo "emu: no display -- starting '$avd' headless on the GPU ($display)" >&2
|
||||
DISPLAY="$display" "$sdk/emulator/emulator" -avd "$avd" \
|
||||
-gpu host $GPU_HOST_FEATURES -no-audio -no-window >"$log" 2>&1 &
|
||||
else
|
||||
echo "emu: no display -- starting '$avd' headless" >&2
|
||||
echo "emu: no display and no compositor -- starting '$avd' on software rendering" >&2
|
||||
"$sdk/emulator/emulator" -avd "$avd" -gpu swiftshader_indirect -no-audio -no-window \
|
||||
>"$log" 2>&1 &
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# The compositor `emu` starts when this machine has no display, so that the
|
||||
# Android emulator has an X server to talk to.
|
||||
#
|
||||
# Nothing here is meant to be looked at; there is no screen. The point is
|
||||
# Xwayland: the emulator's `-gpu host` renderer speaks GLX, and with no
|
||||
# DISPLAY at all it falls back to a software rasteriser. Xwayland's GLX is
|
||||
# backed by this VM's virtio-gpu render node, so the guest's GLES ends up on
|
||||
# the host's real GPU.
|
||||
#
|
||||
# `xwayland force` starts it immediately rather than at the first X client,
|
||||
# so `emu` can read DISPLAY out of a running compositor instead of racing it.
|
||||
xwayland force
|
||||
|
||||
# Sized to a phone so the emulator's window is never scaled or clipped. It is
|
||||
# larger than any AVD here; an output smaller than the window is the one way
|
||||
# this arrangement can affect what the emulator renders.
|
||||
output HEADLESS-1 mode 1440x3120@60Hz
|
||||
|
||||
# No bar, no wallpaper, no keybindings, no pointer follow: each of those is a
|
||||
# client to start or a thing to go wrong on a machine with nobody at the
|
||||
# keyboard, and none of them has anything to draw on.
|
||||
default_border none
|
||||
focus_follows_mouse no
|
||||
Reference in new issue
Block a user