com.harryaskham.waylandroid · Kotlin · C

Waylandroid

An Android app with a Wayland compositor built in. Run Sway or Hyprland from Nix-on-Droid and Waylandroid shows the whole desktop in a floating window or fullscreen. GPU apps inside it are drawn on the phone's GPU and handed to Android with one GPU copy and no CPU copies.

It is the Wayland sibling of X11 Droid, which does the same for X11 programs, and it shares its floating windows, its zero-copy bridge and its remote-control listener with it.

Hyprland's default wallpaper behind a floating window running vkcube, the spinning LunarG cube, captured from Waylandroid's zero-copy ring.
Hyprland running vkcube, captured from the ring.

How it fits together

Where each part runs Waylandroid's place. Inside Nix-on-Droid, a separate Android app running Linux userspace under proot, the user runs Sway or Hyprland. They connect, as ordinary Wayland clients, to a socket that the bridge listens on and hands each connection to Waylandroid, an Android app with a Wayland compositor built in. The nested desktop presents its whole output as one surface. Waylandroid shows it on a SurfaceControl layer in a floating window or fullscreen, and Android touch, keyboard and mouse go back to the desktop as Wayland input events. GPU appsvkcube · kitty · rofi Sway or Hyprlandyour own config waylandroid-0$XDG_RUNTIME_DIR socket Waylandroidlibwayland-server inside SurfaceControl layerfloat or fullscreen touch · keysmouse · scroll NIX-ON-DROID (PROOT) ANDROID each connection handed over wl_seat
The desktop is nested. Sway or Hyprland run as an ordinary Wayland client of Waylandroid and present their whole output as one surface.

Waylandroid is the host compositor: it owns the Android surface, the GPU presentation and the input. Sway and Hyprland run inside Nix-on-Droid, a separate app that provides Linux userspace under proot, and connect to Waylandroid exactly as they would to any parent compositor. Each reads your own configuration, with no generated config or injected arguments.

Clients reach Waylandroid through a socket at $XDG_RUNTIME_DIR/waylandroid-0. Each connection is passed across to the Android app with its file descriptor and adopted by the compositor there, so the desktop speaks Wayland directly to it, with no waypipe and no network relay.

Waylandroid's app screen with a nested Wayland desktop open in a floating window, a terminal inside it, and a list of presets below.
A nested desktop in a float.
Red, green and blue gears rendered by a GL demo inside the nested desktop.
A GL demo inside it.

Zero-copy frames

A Linux desktop compositor expects three things an Android app does not have: a DRM device to allocate buffers on, a GPU driver that understands Linux DMA-BUFs, and its clients in the same process or user. Turnip, Mesa's open-source Vulkan driver for Adreno GPUs, makes it possible: it runs on Android's own kernel interface, exports its images as DMA-BUFs and imports Android's gralloc buffers, which are DMA-BUFs underneath. Everything else arranges for Turnip to do the GPU work on both sides.

One frame, end to end The path of one frame. A GPU app renders with Vulkan, or with OpenGL through zink, on Turnip, Mesa's driver for Adreno GPUs, into a buffer exported as a DMA-BUF. The desktop imports it and composites its output into another DMA-BUF, allocated through Vulkan because there is no DRM device. Waylandroid passes that buffer to the bridge, which waits on its fence and copies it with one GPU blit into a free slot of the app's four-buffer ring. Waylandroid shows the slot on a SurfaceControl layer, and Android's compositor puts it on the panel. Pixels are written by the GPU three times and never by the CPU. app rendersTurnip · DMA-BUF desktop compositeslinux-dmabuf v3 output bufferVulkan, no GBM one blitvkCmdBlitImage ring slotSurfaceControl panelSurfaceFlinger Linux side the only copy Android
One blit, no CPU copies. Only file descriptors, ring indices and fences cross between processes.

Compositing without a DRM device

  • wlroots (Sway): a patch, enabled with WLR_VK_NO_DRM=1, picks the Vulkan device directly, allocates output buffers through Vulkan instead of GBM, and offers clients linux-dmabuf version 3, which needs no device.
  • Hyprland renders with OpenGL ES on a surfaceless display, turned into Vulkan by zink. Its backend library gets the same kind of allocator (AQ_VK_NO_DRM) and fixes for running nested, and it runs at the host's refresh rate, passed in as WAYLANDROID_REFRESH_HZ.
  • OpenGL apps stay on the GPU through a small Mesa patch that keeps zink on the device-less path.

Getting frames across apps

Waylandroid owns a ring of four gralloc buffers, sized to the view they are shown in. When the desktop commits a frame, the bridge waits on its fence and copies it into a free slot with one vkCmdBlitImage, and Waylandroid shows the slot on a child SurfaceControl layer.

How the two apps meet The bridge handshake. SELinux keeps the two apps from sharing a socket, so the Linux side starts a small Java helper with Android's app_process, running as Nix-on-Droid's user. The helper sends Waylandroid a Binder in a broadcast. Waylandroid checks the launch's token and the caller's user id, then sends back its ring of buffers. The helper passes each buffer to the native bridge over an inherited Unix socket. From then on only buffer handles, ring indices and release notices cross between them. Binder in a broadcast1 check token and UID the four ring buffers2 each buffer's DMA-BUF3 a desktop frame's DMA-BUF4 PRESENT(slot)5 bridge helperapp_process, Nix-on-Droid's UIDbridge helperapp_process, Nix-on-Droid's UID Waylandroidowns the ringWaylandroidowns the ring native bridgeblits framesnative bridgeblits frames
A Binder in a broadcast. SELinux keeps the two apps off a shared socket, so they meet through Android's own IPC, checked by token and user id.

Measured

On a RedMagic Astra (Snapdragon 8 Elite, Adreno 830, 165 Hz), Turnip from Mesa 26.2, Waylandroid 0.2.54
SetupRunning insideRateCPU copies
Hyprland docked, launched from the appvkcube163 fps0
GPU Sway docked, launched from the appvkcube164–166 fps0
Hyprland in a Waylandroid floatvkcube, kitty, rofi60 fps0

vkcube's frames were counted with WAYLAND_DEBUG and the bridge's own counters, read through the app's control API, while the panel's active mode was read every second. 165 Hz needs the phone's refresh rate fixed at 165: its automatic mode holds apps at 60 except just after a touch, and the float row was measured before that was known.

Behaving like an Android app

Limits

Trying it

  1. Install Waylandroid (and X11 Droid for X11 programs) from the Play internal track, with Nix-on-Droid.
  2. Under Connectivity, prepare the native display tools, built from the app's own sources.
  3. Turn on zero-copy GPU apps, then build the GPU desktop.
  4. Tap Sway or Hyprland; Display chooses docked or floating.
# or from a Nix-on-Droid shell
waylandroid-sway         # GPU Sway on the direct display
waylandroid-hyprland     # Hyprland (needs the GPU desktop)
vkcube --wsi wayland     # inside either