OpenGL ES 2/3 + EGL over IPC. The same split-loader idea as vkproxy, but
for GLES: a client process whose GL stack can't load the real driver in its
own address space ships every GL call over a socket to a server that
actually owns an EGL context on the GPU.
The packaging is a stack of three drop-in .sos the client side loads
instead of the real ones:
┌──────────────────────────────────┐
│ app / engine │
│ │ │
│ ▼ │
│ libEGL_proxy.so ← fakes EGL: context/config/surface are sentinels; ─┐
│ │ intercepts gbm + wl_egl_window so any "native │
│ │ window" the app makes becomes a wayland-dmabuf │ client
│ │ surface the proxy owns │ process
│ ▼ │
│ libGLESv2_proxy.so → every glXxx() marshals its args and writes them │
│ │ on a UNIX socket via glp_send_cmd / glp_call │
└─────┼──────────────────────────────────────────────────────────────────────┘
│ AF_UNIX (/tmp/husky-gl.sock)
┌─────▼──────────────────────────────┐
│ glproxy-srv (Bionic chroot) │
│ dispatch_gen.c / dispatch_manual.c → real call into g_pfn │
│ egl_init.c → dlopen libEGL_mali / libGLES_mali, create EGL context │
│ dmabuf_output.c → render the result into a wl-dmabuf the client │
│ passed in via SCM_RIGHTS │
└────────────────────────────────────┘
Two key differences from vkproxy:
- EGL is faked client-side, not proxied. The client never talks to the
real EGL.
libEGL_proxy.sohands back constant sentinel handles forEGLDisplay/EGLConfig/EGLContext/EGLSurface, intercepts the GBM and Wayland-EGL helper calls (gbm_surface_*,wl_egl_window_*) so the app's "native window" plumbing routes through the proxy instead. The server owns the real EGL context. - Output is a shared dmabuf, not a swapchain. The client gives the
server a dmabuf fd (allocated via
gbmorlinux-dmabuf-v1); the server'sglDrawArrays/glFinish/eglSwapBuffersequivalents render into a renderbuffer backed by that same dmabuf, and the client's Wayland compositor sees the result as soon asglp_presentreturns.
glproxy/
├── Makefile # 3 targets: libGLESv2_proxy / libEGL_proxy / glproxy-srv
├── redeploy.sh # codegen → make → scp libs + binary to phone
├── codegen/
│ └── gen.py # walks gl.xml, emits opcode enum + client stubs + server dispatch
├── registry/
│ └── gl.xml # the Khronos OpenGL registry (drives codegen)
├── include/
│ ├── glproxy_proto.h # opcode enum (codegen'd) + the wire structs for synthetic ops
│ ├── glp_ext.h # public symbols clients/EGL-shim can call into the proxy with
│ └── gl_pixel_size.h # GL_RGBA/GL_UNSIGNED_BYTE → bytes-per-pixel lookup
├── protocol/
│ ├── linux-dmabuf-unstable-v1-protocol.c # generated by wayland-scanner
│ └── linux-dmabuf-unstable-v1-client-protocol.h
├── client/ # everything that ends up in the *_proxy.so libraries
│ ├── transport.c/.h # AF_UNIX framing: glp_send_cmd / glp_call / SCM_RIGHTS fd send
│ ├── glesv2_stubs.c # codegen'd: every glXxx() → marshal → send → (maybe) recv
│ ├── manual_stubs.c # hand-written overrides for variable-size payloads
│ │ (glBufferData, glShaderSource, glTexImage2D, etc.)
│ ├── egl_shim.c # libEGL_proxy entry points: eglGetDisplay / eglCreateContext
│ │ / eglMakeCurrent / eglSwapBuffers — all use sentinel handles
│ ├── gbm_shim.c # overrides gbm_surface_* so apps using GBM-backed EGL surfaces
│ │ get a proxy-owned wayland-dmabuf surface instead
│ └── wayland_shim.c # overrides wl_egl_window_* (the wayland-egl helper) — same idea
├── server/ # the Bionic-side daemon
│ ├── main.c # AF_UNIX listener, per-client thread, hdr/payload framing
│ ├── server.h # public glp_* server API (egl init, present, dmabuf, EGLImage)
│ ├── dispatch_gen.c # codegen'd: case OP_glXxx → unmarshal → g_pfn.glXxx → reply
│ ├── dispatch_manual.c # hand-written dispatch for synthetic ops + variable-size payloads
│ ├── gl_funcs.c/.h # codegen'd: struct of every PFN_glXxx loaded via eglGetProcAddress
│ ├── egl_init.c # dlopen libEGL_mali / libGLES_mali, eglInitialize, eglBindAPI,
│ │ create a real EGLContext we keep current on every thread
│ ├── dmabuf_output.c # accept an SCM_RIGHTS dmabuf, attach as renderbuffer via
│ │ EGL_LINUX_DMA_BUF_EXT → EGLImage → glFramebufferTexture2D
│ └── image_proxy.c # the EGLImage object table — clients see opaque uint64 handles
└── tests/ # all standalone
├── arm64_smoke.c # link against libGLESv2_proxy.arm64.so and call a few entrypoints
├── egl_smoke.c # exercises the eglCreatePlatformWindow path
├── triangle_smoke.c # actually draws a triangle, reads pixels, checks colours
├── spin_smoke.c # spinning quad — useful for visually confirming present timing
├── scanout_smoke.c # dmabuf → wayland scanout end-to-end
├── phase25_smoke.c # phase-25 (uniform buffer object) sanity test
├── smoke_client.c # bare wire-protocol round trip
├── dmabuf_smoke.py # python dmabuf-import smoke
├── fakesrv.py # fake server that just echoes — used by client unit tests
└── shader_smoke.py # compile-and-link a shader through the proxy
Same shape as vkproxy: fixed cmd-header, payload, optional ancillary fd
via SCM_RIGHTS. The server replies in-line for ops that produce a
return value (glCreateShader, glGetAttribLocation, glGetIntegerv,
…); pure void ops are fire-and-forget.
Opcode space:
| Range | Meaning |
|---|---|
0x0000 – 0x7FFF |
One-to-one mirror of a GL command — emitted by gen.py |
0x8000 – 0xFFFF |
Synthetic ops handled by dispatch_manual.c |
The synthetic range is short and proxy-specific:
OP_glp_set_output_dmabuf— client says "this fd is my render target; it's WxH, stride S, fourcc F". Server makes anEGLImagefrom it and binds it as the colour attachment of an FBO. SCM_RIGHTS carries the fd.OP_glp_present— finish + signal Wayland. The serverglFinish()'s, the dmabuf is then ready for the client compositor.OP_glp_set_panel_swizzle— toggle a server-sideRGBA→BGRAblit step for panels with a swapped channel order.OP_glp_image_create_dmabuf/OP_glp_image_destroy/OP_glp_image_target_texture_2d/OP_glp_image_target_renderbuffer— the proxy's EGLImage object table. The client deals in opaque uint64 handles; the server side maps them to realEGLImagevalues.OP_glp_query_dmabuf_formats/OP_glp_query_dmabuf_modifiers— forwardeglQueryDmaBufFormatsEXT/eglQueryDmaBufModifiersEXT.
glDrawArrays(GL_TRIANGLES, 0, 3) — the simplest possible case:
- App calls
glDrawArrays. The linker resolves this againstlibGLESv2_proxy.arm64.so(named with-Wl,-soname,libGLESv2.so.2so it can be dropped in as a plain libGLESv2 replacement). - The codegen'd stub in
glesv2_stubs.cpacks(GL_TRIANGLES, 0, 3)into a 12-byte buffer and callsglp_send_cmd(OP_glDrawArrays, 0, buf, 12). No reply expected. transport.cwrites the cmd header and payload to/tmp/husky-gl.sock.glproxy-srv's per-client thread reads the header, dispatches: manual ops first (none match), then the codegen'd switch indispatch_gen.c. That switch unpacks the 12 bytes and callsg_pfn.glDrawArrays(mode, first, count).g_pfn.glDrawArraysis the function pointer thategl_init.cpulled out of the reallibGLES_mali.soat startup viaeglGetProcAddress.
For commands that return a value — GLuint glCreateShader(GLenum) —
the client stub uses glp_call (blocking) instead of glp_send_cmd.
The server's dispatcher writes the return value as the reply payload.
For commands with variable-size data — glBufferData,
glShaderSource, glTexImage2D — codegen punts and the hand-written
implementation in manual_stubs.c packs a custom layout: small header
followed by the variable blob.
This is the part that differs most from vkproxy. There's no Vulkan
VkSwapchainKHR equivalent; instead:
- Client (or
libEGL_proxy'swayland_shim.c/gbm_shim.c) allocates a Wayland-attached dmabuf for the surface — typically vialinux-dmabuf-v1andgbm_bo_*on a vendor render node. - Client sends
OP_glp_set_output_dmabufwith the fd + dimensions. - Server
egl_init.ccallseglCreateImageKHRwithEGL_LINUX_DMA_BUF_EXTto wrap the dmabuf, thenglEGLImageTargetTexture2DOES+glFramebufferTexture2Dto bind it as the colour attachment of a server-owned FBO. - Every subsequent client
glDrawArrays/glDrawElementslands on that FBO. - Client calls
OP_glp_present. Server doesglFinish(). The Wayland buffer is now ready —wl_surface_commiton the client side (handled insidewayland_shim.c) makes it visible.
gbm_shim.c and wayland_shim.c intercept the standard helper APIs
(gbm_surface_lock_front_buffer, wl_egl_window_create, etc.) so apps
that bring their own GBM/Wayland-EGL window plumbing don't have to
change.
Reads registry/gl.xml (the Khronos GL registry). Walks every
<command> and classifies args the same way vkproxy does:
- Scalars / GLenum / GLuint → 1:1 marshal.
- Small fixed-size struct pointers → struct-copy.
- Anything variable-length, sentinel-terminated, format-dependent, or
pNext-shaped → emit a
glp_log_unimplstub. Those commands get hand-written inmanual_stubs.c.
Outputs:
include/glproxy_proto.h— opcode enum + synthetic-op structs.client/glesv2_stubs.c— every codegen'dglXxx.server/dispatch_gen.c— the big switch.server/gl_funcs.c/.h—struct gl_funcs g_pfnof everyPFN_glXxx, plus a populator that callseglGetProcAddressfor each.
Regenerate with make regen (just python3 codegen/gen.py).
make # builds:
# build/libGLESv2_proxy.so (x86_64 glibc — for local smoke tests on WSL)
# build/libGLESv2_proxy.arm64.so (aarch64 glibc — drops onto the device)
# build/libEGL_proxy.arm64.so (aarch64 glibc — drops onto the device)
# build/glproxy-srv (aarch64 Android Bionic — runs in chroot)
# build/arm64_smoke (cross-built ABI test)
make regen # python3 codegen/gen.pyCompilers (set in the Makefile):
CC_AND=aarch64-linux-android29-clangfrom$NDK/toolchains/...for the Bionic server.CC_ARM64=aarch64-linux-gnu-gccfor the glibc client.sos.CC_HOST= hostccfor the WSL smoke build.
The arm64 client libGLESv2_proxy.arm64.so is built with
-Wl,-soname,libGLESv2.so.2 and libEGL_proxy.arm64.so with
-Wl,-soname,libEGL.so.1 so they can be linked in as the system libGLES /
libEGL replacements without any source-side changes.
redeploy.sh is the single-step deploy:
python3 codegen/gen.py— regenerate stubs in case anything moved.make -j4— build all four targets.scp build/glproxy-srv root@$PHONE:.../glproxy-srv.newthenmv(atomic — the running server can be replaced in place; systemdRestart=on-failurebrings it back up on the new binary).scpthe two client.sos into/usr/local/lib/glproxy/.
Server-side install paths:
| Target | Path on the GPU-side machine |
|---|---|
glproxy-srv |
/var/lib/machines/halium/usr/local/bin/glproxy-srv (runs in chroot) |
libGLESv2_proxy.arm64.so |
/usr/local/lib/glproxy/libGLESv2_proxy.arm64.so |
libEGL_proxy.arm64.so |
/usr/local/lib/glproxy/libEGL_proxy.arm64.so |
The server is run by systemd as glproxy-srv.service:
[Service]
Type=simple
ExecStart=/usr/bin/chroot /var/lib/machines/halium /usr/local/bin/glproxy-srv
Restart=on-failure
Environment=GLP_DRAW_DEBUG=1 # via drop-in: log every drawTo make a client process pick up the proxy in place of the system
libGLES, point its linker at the proxy lib via LD_LIBRARY_PATH or
/etc/ld.so.conf.d/ — the libraries' soname (libGLESv2.so.2 /
libEGL.so.1) lets them satisfy the same DT_NEEDED entries the real
ones do.
| Variable | Side | Effect |
|---|---|---|
GLPROXY_SOCKET |
both | UDS path (default /tmp/husky-gl.sock) |
GLPROXY_DEBUG_SHADERS |
client | dump every glShaderSource chunk to stderr |
GLP_DRAW_DEBUG |
server | log per-draw info (set by the systemd unit's drop-in) |
- Mali user-space race:
libGLES_mali.so:sub_1A65300walks a registry that can be NULL'd by a concurrent server thread; the workaround (also used by vkproxy) is to serialise dispatch under a single mutex inserver/main.c. Lower throughput, no crash. -Wl,-Bsymbolicon the client libs — same reason as vkproxy: the client.sos must bind their ownglXxxsymbols locally, or the loader's libGLES (loaded earlier) will interpose.- EGL handles are constant sentinels (
0xDEADD15D,0xC07E0001, …). Apps that do pointer-arithmetic onEGLContextare not supported. None do, but the assumption is worth knowing.