Skip to content

Tenser-Linux/glproxy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

glproxy

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.so hands back constant sentinel handles for EGLDisplay / 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 gbm or linux-dmabuf-v1); the server's glDrawArrays/glFinish/eglSwapBuffers equivalents render into a renderbuffer backed by that same dmabuf, and the client's Wayland compositor sees the result as soon as glp_present returns.

Source tree

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

Wire protocol

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 an EGLImage from it and binds it as the colour attachment of an FBO. SCM_RIGHTS carries the fd.
  • OP_glp_present — finish + signal Wayland. The server glFinish()'s, the dmabuf is then ready for the client compositor.
  • OP_glp_set_panel_swizzle — toggle a server-side RGBA→BGRA blit 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 real EGLImage values.
  • OP_glp_query_dmabuf_formats / OP_glp_query_dmabuf_modifiers — forward eglQueryDmaBufFormatsEXT / eglQueryDmaBufModifiersEXT.

How a call flows

glDrawArrays(GL_TRIANGLES, 0, 3) — the simplest possible case:

  1. App calls glDrawArrays. The linker resolves this against libGLESv2_proxy.arm64.so (named with -Wl,-soname,libGLESv2.so.2 so it can be dropped in as a plain libGLESv2 replacement).
  2. The codegen'd stub in glesv2_stubs.c packs (GL_TRIANGLES, 0, 3) into a 12-byte buffer and calls glp_send_cmd(OP_glDrawArrays, 0, buf, 12). No reply expected.
  3. transport.c writes the cmd header and payload to /tmp/husky-gl.sock.
  4. glproxy-srv's per-client thread reads the header, dispatches: manual ops first (none match), then the codegen'd switch in dispatch_gen.c. That switch unpacks the 12 bytes and calls g_pfn.glDrawArrays(mode, first, count).
  5. g_pfn.glDrawArrays is the function pointer that egl_init.c pulled out of the real libGLES_mali.so at startup via eglGetProcAddress.

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.

Presentation

This is the part that differs most from vkproxy. There's no Vulkan VkSwapchainKHR equivalent; instead:

  1. Client (or libEGL_proxy's wayland_shim.c / gbm_shim.c) allocates a Wayland-attached dmabuf for the surface — typically via linux-dmabuf-v1 and gbm_bo_* on a vendor render node.
  2. Client sends OP_glp_set_output_dmabuf with the fd + dimensions.
  3. Server egl_init.c calls eglCreateImageKHR with EGL_LINUX_DMA_BUF_EXT to wrap the dmabuf, then glEGLImageTargetTexture2DOES+glFramebufferTexture2D to bind it as the colour attachment of a server-owned FBO.
  4. Every subsequent client glDrawArrays/glDrawElements lands on that FBO.
  5. Client calls OP_glp_present. Server does glFinish(). The Wayland buffer is now ready — wl_surface_commit on the client side (handled inside wayland_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.

Codegen (codegen/gen.py)

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_unimpl stub. Those commands get hand-written in manual_stubs.c.

Outputs:

  • include/glproxy_proto.h — opcode enum + synthetic-op structs.
  • client/glesv2_stubs.c — every codegen'd glXxx.
  • server/dispatch_gen.c — the big switch.
  • server/gl_funcs.c/.hstruct gl_funcs g_pfn of every PFN_glXxx, plus a populator that calls eglGetProcAddress for each.

Regenerate with make regen (just python3 codegen/gen.py).

Build

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.py

Compilers (set in the Makefile):

  • CC_AND = aarch64-linux-android29-clang from $NDK/toolchains/... for the Bionic server.
  • CC_ARM64 = aarch64-linux-gnu-gcc for the glibc client .sos.
  • CC_HOST = host cc for 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.

Deploy

redeploy.sh is the single-step deploy:

  1. python3 codegen/gen.py — regenerate stubs in case anything moved.
  2. make -j4 — build all four targets.
  3. scp build/glproxy-srv root@$PHONE:.../glproxy-srv.new then mv (atomic — the running server can be replaced in place; systemd Restart=on-failure brings it back up on the new binary).
  4. scp the 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 draw

To 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.

Runtime knobs

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)

Known sharp edges

  • Mali user-space race: libGLES_mali.so:sub_1A65300 walks 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 in server/main.c. Lower throughput, no crash.
  • -Wl,-Bsymbolic on the client libs — same reason as vkproxy: the client .sos must bind their own glXxx symbols locally, or the loader's libGLES (loaded earlier) will interpose.
  • EGL handles are constant sentinels (0xDEADD15D, 0xC07E0001, …). Apps that do pointer-arithmetic on EGLContext are not supported. None do, but the assumption is worth knowing.

About

opengl proxy to bridge the GPU connection from glibc to the bionic Mali library.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages