From 56af02f37e58bc7c6d5b21559ccbaa4325b057ae Mon Sep 17 00:00:00 2001 From: s1oz Date: Mon, 21 Sep 2026 17:11:36 +0800 Subject: [PATCH 1/2] feat: optional host GPU via GPU_MODE=host Keep the default SwiftShader path. When GPU_MODE=host and /dev/dri is passed through, start a headless modesetting Xorg for Mesa GLX and load an EGL GBM hook so gfxstream can use the host GPU without a monitor. --- Dockerfile | 27 ++++++++++++++ README.md | 35 ++++++++++++++++++ docker-compose.yml | 4 +++ egl-gbm-hook.c | 89 ++++++++++++++++++++++++++++++++++++++++++++++ start-emulator.sh | 70 ++++++++++++++++++++++++++++++++++-- xorg-headless.conf | 44 +++++++++++++++++++++++ 6 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 egl-gbm-hook.c create mode 100644 xorg-headless.conf diff --git a/Dockerfile b/Dockerfile index eb1d528..f59028a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -83,6 +83,33 @@ COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY first-boot.sh /root/first-boot.sh RUN chmod +x /root/first-boot.sh +# Host GPU userspace (Intel/AMD/NVIDIA via Mesa). Used when GPU_MODE=host. +# Own layer so the SDK download above stays cached. +COPY egl-gbm-hook.c /tmp/egl-gbm-hook.c +COPY xorg-headless.conf /etc/X11/xorg-headless.conf +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + gcc \ + libgl1-mesa-dri \ + libgl1 \ + libglx-mesa0 \ + libgles2 \ + libegl1-mesa \ + mesa-vulkan-drivers \ + libvulkan1 \ + xvfb \ + xserver-xorg-core \ + xserver-xorg-video-intel \ + x11-xserver-utils \ + libxtst6 \ + libglu1-mesa \ + libxv1 && \ + gcc -shared -fPIC -O2 -o /usr/local/lib/libegl-gbm-hook.so /tmp/egl-gbm-hook.c -ldl && \ + rm -f /tmp/egl-gbm-hook.c && \ + apt-get purge -y --auto-remove gcc && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + # Copy the start-emulator script COPY start-emulator.sh /root/start-emulator.sh RUN chmod +x /root/start-emulator.sh diff --git a/README.md b/README.md index 61b80ad..d6523dc 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Access and control the Android emulator directly in your web browser with the in - **Easy Setup:** Simple Docker commands to build and run the emulator. - **Supervisor Management:** Manages emulator processes with Supervisor for reliability. - **Unified Container Logs:** All emulator and boot logs are redirected to Docker's standard log system. +- **Optional Host GPU:** Set `GPU_MODE=host` and pass `/dev/dri` to render guest GLES on the host GPU (Intel/AMD/NVIDIA via Mesa) instead of SwiftShader. ## 🛠️ **Prerequisites** @@ -129,6 +130,32 @@ List of devices attached localhost:5555 device ``` +### Host GPU (`GPU_MODE=host`) + +The default renderer is SwiftShader (CPU). To use an Intel/AMD/NVIDIA GPU: + +1. Pass the DRM device into the container (`/dev/dri`). +2. Set `GPU_MODE=host`. +3. If the container is not privileged, add the host GID that owns `/dev/dri` via `group_add`. + +```yaml +environment: + GPU_MODE: host +devices: + - /dev/kvm + - /dev/dri:/dev/dri +# group_add: +# - "44" # `stat -c %g /dev/dri/renderD128` +``` + +Headless hosts do not need a monitor. The emulator still runs `-no-window`; viewing stays on scrcpy / scrcpy-web. A small Xorg on `/dev/dri/card0` provides GLX (Mesa iris/amdgpu/nouveau). Confirm the guest actually picked the GPU: + +```bash +adb shell dumpsys SurfaceFlinger | grep GLES +``` + +You should see the host GPU name (for example `Mesa Intel(R) UHD Graphics 630`), not `Google SwiftShader`. + ### Use scrcpy to Mirror the Emulator Screen For a native desktop experience, you can use scrcpy: @@ -150,6 +177,7 @@ scrcpy -s localhost:5555 | `ROOT_SETUP` | Set to `1` to enable rooting and Magisk. Can be turned on after the first start but cannot be undone without recreating the data volume. | `0` | | `GAPPS_SETUP` | Set to `1` to install PICO GAPPS. Can be turned on after the first start but cannot be undone without recreating the data volume. | `0` | | `ARM_TRANSLATION` | Set to `1` to enable ARM translation (ndk_translation) for running ARM/ARM64 apps on x86_64. Can be turned on after the first start but cannot be undone without recreating the data volume. | `0` | +| `GPU_MODE` | Emulator GPU backend. `swiftshader_indirect` (default) is software rendering. `host` uses the host GPU through Mesa. | `swiftshader_indirect` | ## 🔄 **First Boot Process** @@ -222,6 +250,7 @@ Builds from the `main` branch are published as development images using `edge` a - [x] Support Magisk - [x] Adding web interface of [scrcpy](https://github.com/Shmayro/ws-scrcpy-docker) - [x] Redirect all logs to container stdout/stderr +- [x] Optional host GPU (`GPU_MODE=host`) ## 🐞 **Troubleshooting** @@ -256,6 +285,12 @@ Builds from the `main` branch are published as development images using `edge` a Should show: `x86_64,x86,arm64-v8a,armeabi-v7a,armeabi` - If you enabled `ARM_TRANSLATION` after the first boot, restart the container to run the install step +- **Guest GLES shows Google SwiftShader:** + - `GPU_MODE` defaults to software. Set `GPU_MODE=host` and mount `/dev/dri`. + - Check the container can see the render node: `docker exec dockerify-android ls -l /dev/dri`. + - Confirm logs contain `Starting headless Xorg` and `Graphics Adapter ... Mesa`, not `llvmpipe`. + - Rebuild the image after this change (`docker compose build`); Mesa/Xorg are not in older images. + - **Emulator Not Starting:** - **Check Container Logs:** diff --git a/docker-compose.yml b/docker-compose.yml index acb0be9..b1f11ff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,9 +20,13 @@ services: ROOT_SETUP: 0 # set to 1 to enable rooting GAPPS_SETUP: 0 # set to 1 to install PICO GAPPS ARM_TRANSLATION: 1 # set to 1 to enable ARM translation (allows ARM64 apps) + # GPU_MODE: host # use host GPU (needs /dev/dri below); default is swiftshader_indirect privileged: true devices: - /dev/kvm + # - /dev/dri:/dev/dri # required for GPU_MODE=host + # group_add: + # - "44" # host GID of the group that owns /dev/dri (often 'video' or 'render') scrcpy-web: container_name: scrcpy-web diff --git a/egl-gbm-hook.c b/egl-gbm-hook.c new file mode 100644 index 0000000..79a3b74 --- /dev/null +++ b/egl-gbm-hook.c @@ -0,0 +1,89 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +typedef void *EGLDisplay; +typedef void *EGLNativeDisplayType; +typedef int EGLint; +typedef unsigned int EGLenum; +#define EGL_NO_DISPLAY ((EGLDisplay)0) +#define EGL_PLATFORM_GBM_KHR 0x31D7 + +struct gbm_device; + +static void hooklog(const char *msg) { + FILE *f = fopen("/tmp/egl-hook.log", "a"); + if (!f) + return; + fprintf(f, "%s\n", msg); + fclose(f); +} + +static void *real_from(const char *lib, const char *sym) { + void *h = dlopen(lib, RTLD_NOLOAD | RTLD_NOW); + if (!h) + h = dlopen(lib, RTLD_NOW); + return h ? dlsym(h, sym) : NULL; +} + +static EGLDisplay gbm_display(void) { + static EGLDisplay dpy = EGL_NO_DISPLAY; + static int inited; + struct gbm_device *(*gbm_create_device)(int); + EGLDisplay (*get_plat)(EGLenum, void *, const EGLint *); + struct gbm_device *gbm; + int fd; + char buf[192]; + + if (inited) + return dpy; + inited = 1; + + hooklog("gbm_display()"); + fd = open("/dev/dri/renderD128", O_RDWR); + if (fd < 0) + fd = open("/dev/dri/card0", O_RDWR); + + gbm_create_device = real_from("libgbm.so.1", "gbm_create_device"); + get_plat = real_from("libEGL.so.1", "eglGetPlatformDisplayEXT"); + if (!get_plat) + get_plat = real_from("libEGL.so.1", "eglGetPlatformDisplay"); + + if (!gbm_create_device || !get_plat || fd < 0) { + snprintf(buf, sizeof(buf), "missing gbm/egl/fd fd=%d gbm=%p get=%p", + fd, (void *)gbm_create_device, (void *)get_plat); + hooklog(buf); + return EGL_NO_DISPLAY; + } + + gbm = gbm_create_device(fd); + dpy = get_plat(EGL_PLATFORM_GBM_KHR, gbm, NULL); + snprintf(buf, sizeof(buf), "using GBM fd=%d gbm=%p dpy=%p", fd, (void *)gbm, (void *)dpy); + hooklog(buf); + fprintf(stderr, "egl-gbm-hook: %s\n", buf); + return dpy; +} + +EGLDisplay eglGetDisplay(EGLNativeDisplayType native) { + (void)native; + hooklog("eglGetDisplay"); + return gbm_display(); +} + +void *eglGetProcAddress(const char *name) { + void *(*real)(const char *); + + if (name && strcmp(name, "eglGetDisplay") == 0) + return (void *)eglGetDisplay; + + real = real_from("libEGL.so.1", "eglGetProcAddress"); + return real ? real(name) : NULL; +} + +__attribute__((constructor)) static void init_hook(void) { + hooklog("hook loaded"); +} diff --git a/start-emulator.sh b/start-emulator.sh index 420c88a..87924bd 100755 --- a/start-emulator.sh +++ b/start-emulator.sh @@ -17,8 +17,8 @@ CONFIG_FILE="/data/android.avd/config.ini" update_config() { local key="$1" local value="$2" - if grep -q "^$key=" "$CONFIG_FILE"; then - sed -i "s/^$key=.*/$key=$value/" "$CONFIG_FILE" + if grep -qE "^${key}[[:space:]]*=" "$CONFIG_FILE"; then + sed -i "s/^${key}[[:space:]]*=.*/${key}=${value}/" "$CONFIG_FILE" else echo "$key=$value" >> "$CONFIG_FILE" fi @@ -37,5 +37,69 @@ if [ -f "$CONFIG_FILE" ]; then fi fi +# Default stays software (historical -gpu swiftshader_indirect). +# Set GPU_MODE=host and pass /dev/dri to use the host GPU. +case "${GPU_MODE:-swiftshader_indirect}" in + swiftshader|software) GPU_MODE=swiftshader_indirect ;; + "") GPU_MODE=swiftshader_indirect ;; +esac + +if [ "$GPU_MODE" = "host" ] && [ ! -e /dev/dri/renderD128 ] && [ ! -e /dev/dri/card0 ]; then + echo "GPU_MODE=host requested but no /dev/dri render node; falling back to swiftshader_indirect" + GPU_MODE=swiftshader_indirect +fi + +if [ -f "$CONFIG_FILE" ]; then + update_config "hw.gpu.enabled" "yes" + update_config "hw.gpu.mode" "$GPU_MODE" +fi + +if [ "$GPU_MODE" = "host" ]; then + # Mesa DRI is a system library; the emulator's bundled libc++ will not load it. + export ANDROID_EMULATOR_USE_SYSTEM_LIBS="${ANDROID_EMULATOR_USE_SYSTEM_LIBS:-1}" + export QT_QPA_PLATFORM="${QT_QPA_PLATFORM:-offscreen}" + export LIBGL_ALWAYS_SOFTWARE=0 + export EGL_PLATFORM="${EGL_PLATFORM:-gbm}" + unset GALLIUM_DRIVER + unset MESA_LOADER_DRIVER_OVERRIDE + if [ -z "$VK_ICD_FILENAMES" ] && [ -f /usr/share/vulkan/icd.d/intel_icd.x86_64.json ]; then + export VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/intel_icd.x86_64.json + fi + # gfxstream still opens X11. Prefer modesetting Xorg on /dev/dri/card0 so + # GLX is Mesa iris; Xvfb is llvmpipe-only fallback. Viewing stays on scrcpy. + export DISPLAY="${DISPLAY:-:99}" + disp_num="${DISPLAY#:}" + display_live() { + [ -S "/tmp/.X11-unix/X${disp_num}" ] || return 1 + xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 + } + if ! display_live; then + rm -f "/tmp/.X11-unix/X${disp_num}" + mkdir -p /tmp/.X11-unix /var/log + xorg_conf="${XORG_HEADLESS_CONF:-/etc/X11/xorg-headless.conf}" + if command -v Xorg >/dev/null 2>&1 && [ -e /dev/dri/card0 ] && [ -f "$xorg_conf" ]; then + Xorg "$DISPLAY" -config "$xorg_conf" -ac -noreset -nolisten tcp -logfile /tmp/xorg.log >/tmp/xorg-stdout.log 2>&1 & + echo "Starting headless Xorg $DISPLAY on /dev/dri/card0 (modesetting/glamor)" + else + Xvfb "$DISPLAY" -screen 0 "${XVFB_SCREEN:-1280x720x24}" +extension GLX +extension RANDR +extension RENDER -ac -nolisten tcp >/tmp/xvfb.log 2>&1 & + echo "Starting Xvfb $DISPLAY (software GL fallback)" + fi + for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do + display_live && break + sleep 0.25 + done + fi + if ! display_live; then + echo "Display $DISPLAY did not come up; host GPU GLX will fail" + fi + HOOK="${EGL_GBM_HOOK:-/usr/local/lib/libegl-gbm-hook.so}" + if [ -f "$HOOK" ]; then + export LD_PRELOAD="${HOOK}${LD_PRELOAD:+:$LD_PRELOAD}" + echo "Using EGL GBM hook on /dev/dri/renderD128" + fi +fi + +echo "Starting emulator GPU_MODE=${GPU_MODE} dri=$(ls /dev/dri 2>/dev/null | tr '\n' ' ')" + # Start the emulator with the appropriate ramdisk.img -/opt/android-sdk/emulator/emulator -avd android -nojni -netfast -writable-system -no-window -no-audio -no-boot-anim -skip-adb-auth -gpu swiftshader_indirect -no-snapshot -no-metrics $RAMDISK -qemu -m ${RAM_SIZE:-4096} +/opt/android-sdk/emulator/emulator -avd android -nojni -netfast -writable-system -no-window -no-audio -no-boot-anim -skip-adb-auth -gpu "$GPU_MODE" -no-snapshot -no-metrics $RAMDISK -qemu -m ${RAM_SIZE:-4096} diff --git a/xorg-headless.conf b/xorg-headless.conf new file mode 100644 index 0000000..7605a87 --- /dev/null +++ b/xorg-headless.conf @@ -0,0 +1,44 @@ +# Headless Xorg for GPU_MODE=host. Binds Mesa iris/amdgpu/nouveau to +# /dev/dri/card0 even when no monitor is connected. Not used for viewing; +# the Android screen is still accessed via scrcpy / scrcpy-web. +Section "ServerFlags" + Option "AllowEmptyInitialConfiguration" "true" + Option "AutoAddGPU" "false" + Option "DontVTSwitch" "true" + Option "DontZap" "true" +EndSection + +Section "ServerLayout" + Identifier "Layout0" + Screen 0 "Screen0" + Option "AutoAddDevices" "false" + Option "AutoAddGPU" "false" +EndSection + +Section "Device" + Identifier "Card0" + Driver "modesetting" + Option "AccelMethod" "glamor" + Option "kmsdev" "/dev/dri/card0" + Option "SWcursor" "true" +EndSection + +Section "Monitor" + Identifier "Monitor0" + HorizSync 30-100 + VertRefresh 50-70 + Modeline "1280x720_60.00" 74.48 1280 1336 1472 1664 720 721 724 746 -HSync +Vsync + Option "PreferredMode" "1280x720_60.00" +EndSection + +Section "Screen" + Identifier "Screen0" + Device "Card0" + Monitor "Monitor0" + DefaultDepth 24 + SubSection "Display" + Depth 24 + Modes "1280x720" + Virtual 1280 720 + EndSubSection +EndSection From a1ff01db1fa4553c0283f1433731ae103064810a Mon Sep 17 00:00:00 2001 From: s1oz Date: Mon, 21 Sep 2026 21:38:27 +0800 Subject: [PATCH 2/2] fix: build EGL hook in a gcc stage; drop intel DDX from runtime --no-install-recommends gcc does not pull libc6-dev, so compiling the hook in the runtime layer fails. Build the .so in a tiny stage and keep Mesa/Xorg install noninteractive without purging gcc. --- Dockerfile | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index f59028a..2bdbd61 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,13 @@ +# Compile the EGL hook without leaving gcc in the runtime image. +FROM ubuntu:20.04 AS egl-hook +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + gcc \ + libc6-dev && \ + rm -rf /var/lib/apt/lists/* +COPY egl-gbm-hook.c /tmp/egl-gbm-hook.c +RUN gcc -shared -fPIC -O2 -o /libegl-gbm-hook.so /tmp/egl-gbm-hook.c -ldl + FROM ubuntu:20.04 # Install necessary packages @@ -84,29 +94,28 @@ COPY first-boot.sh /root/first-boot.sh RUN chmod +x /root/first-boot.sh # Host GPU userspace (Intel/AMD/NVIDIA via Mesa). Used when GPU_MODE=host. -# Own layer so the SDK download above stays cached. -COPY egl-gbm-hook.c /tmp/egl-gbm-hook.c +# Own layer so the SDK download above stays cached. modesetting is in +# xserver-xorg-core; the intel DDX package is not required. +COPY --from=egl-hook /libegl-gbm-hook.so /usr/local/lib/libegl-gbm-hook.so COPY xorg-headless.conf /etc/X11/xorg-headless.conf RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - gcc \ + -o Dpkg::Options::=--force-confdef \ + -o Dpkg::Options::=--force-confold \ libgl1-mesa-dri \ libgl1 \ libglx-mesa0 \ libgles2 \ libegl1-mesa \ + libgbm1 \ mesa-vulkan-drivers \ libvulkan1 \ xvfb \ xserver-xorg-core \ - xserver-xorg-video-intel \ x11-xserver-utils \ libxtst6 \ libglu1-mesa \ libxv1 && \ - gcc -shared -fPIC -O2 -o /usr/local/lib/libegl-gbm-hook.so /tmp/egl-gbm-hook.c -ldl && \ - rm -f /tmp/egl-gbm-hook.c && \ - apt-get purge -y --auto-remove gcc && \ apt-get clean && \ rm -rf /var/lib/apt/lists/*