Status: accepted integration boundary, 2026-07-24
uDroid will embed the Termux:X11 lorie library and native X server in the
uDroid APK. It will not require the separate Termux:X11 application, launch an
X server through app_process, or use broadcasts as its internal control
plane.
The first generic graphics backend is the unmodified Android GLES presenter. Device-specific acceleration, including the Tensor G1 Panfrost profile, is an optional layer above the same X11 session contract.
The initial upstream baseline is Termux:X11 commit
0cb0203c283bfafbad380b90444296aa42af058d. This revision split the project
into a reusable :lorie library and a thin :lorie-app wrapper. uDroid will
track the upstream source and carry a small, auditable integration patch set.
Embedding GPLv3 Termux:X11 code changes the distribution obligations of the combined APK. The source import checkpoint must update the root license and third-party notices before a binary containing that code is published.
The pinned :lorie library and its recursive native dependencies now build
inside uDroid with NDK 28.2.13676358 for arm64-v8a, armeabi-v7a, and
x86_64. The merged application manifest removes Termux:X11's standalone
Activity, preferences Activity, receiver, and key-interceptor service, leaving
uDroid as the sole application and lifecycle owner.
The supervised server process now starts the native X server directly through a small uDroid JNI entrypoint. It does not use the old shell loader, hidden Android APIs, broadcasts, a TCP listener, or a separate Termux:X11 installation.
The renderer channel is now transported as a ParcelFileDescriptor over the
same private Binder contract. uDroid owns a minimal display SurfaceView
instead of embedding Termux:X11's Activity, preferences, navigation, or
broadcast lifecycle. Closing the Desktop page detaches that view while the
supervised X server and PRoot guest continue running.
flowchart LR
UI["uDroid desktop Activity"] -->|"attach Surface and IME"| View["Lorie display view"]
Supervisor["Runtime supervisor"] -->|"versioned Binder contract"| Server["X11 server process"]
Server --> Socket["Private X socket"]
Supervisor --> PRoot["PRoot distro"]
Socket -->|"bind-mounted as /tmp/.X11-unix/X0"| PRoot
PRoot -->|"DISPLAY=:0"| Apps["Linux X11 apps"]
Server -->|"buffer and event FD transport"| View
View -->|"EGL/GLES present"| Surface["Android Surface"]
View -->|"pointer, button, key, and UTF-8 text"| Server
RuntimeSupervisorServiceowns desired state, startup order, shutdown, recovery, logs, and health.- The X server runs in an app-private Android process. It may outlive the
desktop Activity and its
Surface. - The desktop Activity owns only the visible Android surface and input/IME adapters. Closing or rotating it must not terminate Linux or the X server.
- The X socket and control socket live in a per-generation private runtime
directory. That directory is bind-mounted into PRoot as
/tmp. - The control protocol uses Binder and file descriptors. It is never exposed on TCP and never accepts an unauthenticated broadcast.
Keep:
- Xorg/Xwayland-independent Lorie X server core
LorieBuffer, DRI3, Present, EXA, RandR, XKB, clipboard, and input protocol- Android EGL/GLES renderer
LorieViewsurface, IME, clipboard, and pointer behavior- pinned Xorg, Pixman, XKB, and transport dependencies
Replace or adapt:
CmdEntryPointshell-side loader- hidden framework APIs and package lookup
- the retrying localhost/broadcast connection handshake
- Termux package paths and assumptions
- upstream Activity/navigation/preferences ownership
- process lifetime inferred from Activity lifetime
The Android side follows four rules:
- A surface can appear, resize, disappear, and reappear without restarting the X server.
- The Android child surface uses the upstream HAL BGRA format contract and a transparent parent drawable. Buffer stride comes from the allocator; width is never assumed to equal stride.
- A buffer is not reused until its release fence or equivalent completion signal is observed.
- The stable generic path is retained whenever a zero-copy or device-specific path fails capability or artifact probes.
The cursor should eventually use an independent Android surface. That keeps pointer movement from damaging or presenting the complete desktop.
The display status header is not part of the X11 surface. It can be collapsed to a 32 dp strip and expanded again without detaching the renderer, resizing the guest more than once, or restarting the supervised X server. Users may also persist a collapsed-by-default preference.
The settings surface follows Termux:X11's output, pointer, keyboard, and session grouping, but only exposes controls backed by the embedded uDroid renderer:
- native, scaled, or fixed 720p/900p/1080p guest geometry;
- preserved aspect ratio or stretch-to-fill presentation;
- nearest-neighbor or bilinear display filtering;
- absolute direct touch or relative trackpad input with adjustable speed;
- hardware-scancode preference and keep-screen-on behavior.
Changes are persisted in app-private preferences and applied to the attached renderer immediately. Geometry changes update both the GLES viewport and X11 RandR screen size. Direct-touch coordinates are transformed through the letterboxed viewport, while trackpad deltas remain relative.
Clipboard synchronization, advanced multi-finger gestures, stylus pressure, and custom resolution entry remain hidden until their embedded backends exist. The supported settings are therefore a smaller contract than the standalone Termux:X11 application rather than inert compatibility switches.
Desktop environments are not the first health test. Each gate records latency, RSS/PSS, CPU time, frame/present counts, and failure reason.
server-start: the native process reaches the Xorg ready state.socket-ready: the privateX0socket exists and accepts a connection.x-query: a tiny client reads the root window geometry and required extensions.test-pattern: the server draws a deterministic pattern with a known checksum and the Android surface receives frames.surface-cycle: attach, resize, detach, and reattach without restarting the server.input-loop: injected pointer and key events are observed by a tiny X client.present-soak: bounded frame pacing and buffer lifetime test.xterm: first real guest application.lightweight-session: first desktop, initially without composition.
GNOME, KDE, browsers, and games remain later macro probes.
The 2026-07-24 Tensor G1 device run passed server startup, protocol setup, visible application presentation, motion, and surface-cycle checks:
| Probe | Result |
|---|---|
| Process isolation | org.randomcoder.udroid:x11 owned the X server |
| Server socket | app-private .X11-unix/X0, mode srwxrwxrwx |
| Protocol setup | X11 11.0 connection setup completed |
| Cold Binder/process setup | 708 ms |
| Native Xorg to protocol-ready | 117 ms |
| Total request to protocol-ready | 825 ms |
| Guest bridge | same socket bind-mounted at /tmp/.X11-unix, DISPLAY=:0 |
| Renderer transport | one Binder-delivered renderer FD per Desktop attach |
| Android display target | 1080×2142 below expanded controls; near-full height with the 32 dp strip |
| Live X11 geometry | raw probe observed 1280×720 in Fixed mode and 1080×2142 after returning to Native |
| Display filtering | nearest/bilinear switched live without restarting either process |
| Visible clients | glxgears and xlogo both reached the Android display |
| Motion probe | 49,254 pixels changed, 11.84% of the sampled gear region in one second |
| Presenter cadence | stabilized at 299–300 frames per five seconds, approximately 60 FPS |
| Surface cycle | X11 PID 31974 survived detach and reattach; the running gears returned |
| Absolute pointer | xeyes tracked taps from the top-left to bottom-right of the Android surface |
| Tap and drag | raw X11 probe received balanced button 1 press/release and held-button motion |
| Relative trackpad | raw probe received relative motion and a balanced tap click |
| Keyboard | codex produced five matching X11 key press/release pairs |
| Android IME | persistent header button opens Gboard; composing text is diffed before UTF-8 injection |
| Stop ownership | uDroid removed both its PRoot guest and :x11 process |
xychart-beta
title "Cold embedded X11 startup on Pixel 6a"
x-axis ["Binder + process", "Native Xorg", "End to end"]
y-axis "Milliseconds" 0 --> 900
bar [708, 117, 825]
The protocol probe deliberately sends the 12-byte X11 connection prefix and
parses the eight-byte setup response. Merely connecting to the Unix socket is
not considered ready; that weaker test initially hid a probe-side
LocalSocket ordering bug.
The presentation investigation used two independent observations. An XWD dump
of the root window contained the expected gears while Android was still black,
which isolated the failure to the Android surface rather than GLX or Xorg. The
final fix matched upstream Lorie's transparent SurfaceView contract; an
opaque parent background had covered the correctly rendered child surface.
An in-flight request gate also prevents two renderer FDs from racing for
Lorie's single renderer socket.
The input-loop gate uses
tools/x11-input-probe.py, a dependency-free
Python client that speaks the minimal X11 core protocol directly. It creates a
window and logs pointer coordinates, button state, focus, and keycodes. This
avoids installing xev, starting a desktop, or relying on the malformed
widgets produced when a minimal rootfs has no X font packages.
The current touch contract is intentionally simple:
- one finger maps to an absolute pointer and button 1;
- movement while held maps to an X11 drag;
- trackpad mode maps finger deltas to relative pointer motion and a tap to button 1;
- physical mouse primary, secondary, tertiary, and wheel events are forwarded;
- Android key events use upstream Termux:X11 conversion;
- IME composing text is incrementally replaced instead of duplicated.
Right-click touch gestures, multi-touch XI events, full stylus pressure/tilt, and clipboard synchronization are not part of this checkpoint.
This checkpoint proves visible GLX plumbing but not hardware acceleration. Ubuntu Jammy's stock Mesa reports:
| GL query | Current result |
|---|---|
| Direct rendering | yes |
| Renderer | llvmpipe (LLVM 15.0.7, 128 bits) |
| Accelerated | no |
| OpenGL | 4.5 compatibility profile, Mesa 23.2.1 |
The strict deterministic checksum, Present soak, and hardware driver
installation remain future gates. Hardware acceleration is intentionally the
last graphics checkpoint: first the generic display, lifecycle, and input
contract must remain stable. That later checkpoint should package the uDroid
GPU profile and require glxinfo -B to report the hardware renderer before
performance conclusions are drawn.
The AOSP Virtualization TerminalApp is a useful reference, but not a usable
backend for uDroid's non-root PRoot product:
- It runs a Debian guest through Android Virtualization Framework and crosvm, not PRoot.
- Its GUI sends an Android
Surfaceto crosvm's display service and forwards input throughVirtualMachineAPIs. - VirGL and gfxstream accelerate
virtio-gpucommands from a VM guest. PRoot applications do not speak that protocol. - The app is built with platform APIs, is privileged, and requests
MANAGE_VIRTUAL_MACHINEandUSE_CUSTOM_VIRTUAL_MACHINE. A normal uDroid release cannot depend on those capabilities.
The reusable design lessons are surface attach/detach independent of VM lifetime, direct Android-surface presentation, allocator stride correctness, a separate cursor surface, explicit display/input configuration, and last-frame preservation while the UI is backgrounded.
Sources inspected: