Skip to content

Tenser-Linux/vkproxy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

2 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

vkproxy

A split Vulkan loader. Lets a Vulkan client run on a GPU whose user-space driver can't be loaded into that client's process β€” for example because the driver is a Bionic-only blob expecting the AOSP hwvulkan HAL ABI while the client is a glibc process, or because the driver lives on a different machine entirely.

vkproxy splits the Vulkan stack into two halves connected by a small RPC:

            Vulkan client process                     Server process
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  app / engine                β”‚         β”‚                             β”‚
   β”‚     β”‚                        β”‚         β”‚   husky_vkserver            β”‚
   β”‚     β–Ό                        β”‚         β”‚   (dlopen's the real        β”‚
   β”‚  libvulkan loader            β”‚   UDS   β”‚    Vulkan driver and        β”‚
   β”‚     β”‚                        │◄───────►│    forwards every call)     β”‚
   β”‚     β–Ό                        β”‚  (or    β”‚                             β”‚
   β”‚  mali_proxy_icd.so           β”‚   TCP)  β”‚                             β”‚
   β”‚  (ICD shim; marshals every   β”‚         β”‚                             β”‚
   β”‚   call onto the wire)        β”‚         β”‚                             β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                           β”‚
                                                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
                                                  β”‚ real GPU driver β”‚
                                                  β”‚ (e.g. Mali on   β”‚
                                                  β”‚  /dev/mali0)    β”‚
                                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Every Vulkan call goes:

  1. The Vulkan loader on the client side picks mali_proxy_icd.so based on the ICD JSON manifest (mali_proxy_icd.json lives under /usr/local/share/vulkan/icd.d/).
  2. The ICD serialises the call's arguments into a wire format and writes them to /tmp/husky-vk.sock (UDS) or a TCP socket if VKPROXY_REMOTE is set.
  3. husky_vkserver reads the request, calls the real driver, writes the reply back.

There are two server binaries from the same dispatch core:

  • husky_vkserver β€” Bionic / aarch64. Built with the Android NDK, runs in a Halium-style chroot where the Mali blob's expected libc and binder are available, dlopen's libGLES_mali.so.
  • husky_vkserver_pc β€” host glibc / x86_64. Talks TCP and dlopen's a stock libvulkan.so.1. Useful for development without a Mali host β€” the wire protocol is the same.

Source tree

vkproxy/
β”œβ”€β”€ Makefile                       # builds server (NDK), ICD (aarch64-gcc), smoke client, PC server
β”œβ”€β”€ codegen/
β”‚   β”œβ”€β”€ gen.py                     # parses vk.xml; emits opcode enum, client stubs, server dispatch, fn table
β”‚   └── vk.xml                     # the Vulkan registry
β”œβ”€β”€ include/                       # headers shared between client & server
β”‚   β”œβ”€β”€ vkproxy_proto.h            # wire framing: vkp_cmd_hdr, vkp_reply_hdr, SYNTH opcode enum
β”‚   β”œβ”€β”€ vkproxy_proto_gen.h        # codegen'd: every Vulkan command -> opcode id
β”‚   β”œβ”€β”€ transport.h                # client-side socket API (vkp_send_cmd / vkp_call / fd-passing)
β”‚   β”œβ”€β”€ udp_frame.h                # optional UDP frame-blob channel (for remote presents)
β”‚   └── hwvulkan.h                 # subset of AOSP <hardware/hwvulkan.h> the server needs
β”œβ”€β”€ protocol/                      # Wayland protocol bindings (xdg-shell, linux-dmabuf), generated
β”œβ”€β”€ client/                        # mali_proxy_icd.so β€” the ICD the Vulkan loader loads
β”‚   β”œβ”€β”€ mali_proxy_icd.json        # ICD manifest installed to /usr/local/share/vulkan/icd.d/
β”‚   β”œβ”€β”€ icd_entry.c                # vk_icd* entry points the loader calls first
β”‚   β”œβ”€β”€ stubs_gen.c                # codegen'd: every Vulkan command -> marshal -> vkp_send_cmd/vkp_call
β”‚   β”œβ”€β”€ manual_stubs.c             # hand-written overrides for everything codegen can't do
β”‚   β”‚                                (vkCreateInstance, vkCreateDevice, vkGetPhys*Properties,
β”‚   β”‚                                 the actually-complex pipeline/descriptor commands, swapchain,
β”‚   β”‚                                 etc.) plus _vkp_manual_override which vkGetDeviceProcAddr
β”‚   β”‚                                must consult before _vkproxy_lookup
β”‚   β”œβ”€β”€ dispatch.h                 # vkp_dispatchable / handle-translation helpers
β”‚   β”œβ”€β”€ transport.c                # AF_UNIX (or TCP via VKPROXY_REMOTE) socket plumbing
β”‚   β”œβ”€β”€ wl_present.c/.h            # Wayland present path (wl_display, wl_buffer construction)
β”‚   β”œβ”€β”€ x_wl_surface.c             # xdg-shell shim used when running under Xwayland
β”‚   β”œβ”€β”€ udp_frame.c                # UDP frame-blob receive plumbing (PC build only)
β”‚   β”œβ”€β”€ udp_shm_present.c/.h       # SHM presentation when frames arrive as raw bytes
β”‚   β”œβ”€β”€ decode_h264.c/.h           # H.264 decode path for PC mode (server side encodes)
β”‚   └── udp_frame.h                # local copy of the wire header
β”œβ”€β”€ server/                        # husky_vkserver (Bionic) + husky_vkserver_pc (glibc)
β”‚   β”œβ”€β”€ main.c                     # vksrv main: AF_UNIX listener, per-client thread, signal handler
β”‚   β”œβ”€β”€ main_pc.c                  # PC variant: TCP listener, no Bionic/AHB paths
β”‚   β”œβ”€β”€ vk_init.c / vk_init.h      # dlopen libGLES_mali.so, get hwvulkan_device_t, populate g_pfn
β”‚   β”œβ”€β”€ vk_init_pc.c               # PC variant: dlopen libvulkan.so.1, vkGetInstance/DeviceProcAddr
β”‚   β”œβ”€β”€ vk_funcs_gen.c/.h          # codegen'd: g_pfn struct of every Vulkan function pointer + populator
β”‚   β”œβ”€β”€ dispatch_gen.c             # codegen'd: case OP_vkXxx -> unmarshal args -> call g_pfn.vkXxx -> reply
β”‚   β”œβ”€β”€ dispatch_manual.c          # hand-written dispatch for OPs that need server-side state
β”‚   β”‚                                (handle table, BC-texture transcode staging, dmabuf alloc via
β”‚   β”‚                                 dma_heap, MMU/registry quirks, etc.) β€” by far the biggest file
β”‚   β”œβ”€β”€ handle_table.c/.h          # 64-bit cookie ↔ dispatchable-handle map (locked, growable)
β”‚   β”œβ”€β”€ encode_h264.c/.h           # PC build only: encode rendered frames for the remote viewer
β”‚   β”œβ”€β”€ udp_frame.c/.h             # UDP send side of frame chunks
β”‚   β”œβ”€β”€ logsink_inline.c           # one-line LOG() macro that writes to stderr (journald in chroot)
β”‚   └── pc_stubs/ue_husky.h        # tiny stub of ue-husky for the PC build (no BC transcoder there)
β”œβ”€β”€ smoke/                         # smoke client used to validate the link layer
β”‚   β”œβ”€β”€ vksmoke.c                  # connect, OP_vkp_hello, print server's "name/api/ext_count"
β”‚   β”œβ”€β”€ logsink.c                  # same LOG() macro
β”‚   β”œβ”€β”€ hwvulkan.h                 # local copy
β”‚   └── Makefile
└── tests/                         # standalone Vulkan triangle/cube tests
    β”œβ”€β”€ mincube.c / mincube2.c     # raw Vulkan + Wayland present (the latter does textured cube)
    β”œβ”€β”€ mincube2.{vert,frag}       # GLSL source
    β”œβ”€β”€ mincube2_{vert,frag}_spv.h # the same as SPIR-V byte arrays
    β”œβ”€β”€ vkp_hello_test.c           # bare vkp_hello round-trip
    └── xdg-shell-{client-protocol.h,protocol.c}   # generated Wayland boilerplate (local copy)

Wire protocol

Both directions use the same simple framing:

client β†’ server                            server β†’ client
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ struct vkp_cmd_hdr             β”‚        β”‚ struct vkp_cmd_hdr (echo opcode) β”‚
β”‚   uint16_t opcode              β”‚        β”‚   flags |= VKP_FLAG_REPLY        β”‚
β”‚   uint16_t flags               β”‚        β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   uint32_t len                 β”‚        β”‚ struct vkp_reply_hdr             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€        β”‚   int32_t  status (VkResult)     β”‚
β”‚ payload (len bytes)            β”‚        β”‚   uint32_t len                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                                          β”‚ payload (len bytes)              β”‚
                                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

flags:

bit name meaning
0 VKP_FLAG_EXPECTS_REPLY client wants a synchronous reply (used by vkp_call)
1 VKP_FLAG_REPLY the frame is a reply (server -> client direction)
2 VKP_FLAG_HAS_FD one fd is being passed via SCM_RIGHTS on this datagram

Opcode space:

Range Meaning
0x0000 – 0x7FFF One-to-one mirror of a real Vulkan command β€” generated by gen.py
0x8000 – 0xFFFF Synthesised / non-Vulkan ops (OP_vkp_*) β€” handled by dispatch_manual.c

The synthesised range covers things that need server-side state or custom marshalling:

  • OP_vkp_hello β€” protocol handshake; returns the GPU's device name, Vulkan API version, and instance extension count.
  • OP_vkp_create_* / OP_vkp_destroy_* for instance, device, queue, command pool/buffer, image, buffer, image view, sampler, etc. The manual server side maintains the handle table and BC-transcode staging registry alongside.
  • OP_vkp_map_memory / OP_vkp_unmap_memory β€” coordinate dmabuf-backed vkAllocateMemory, including sending the heap fd back via SCM_RIGHTS so the client can mmap the same memory.
  • OP_vkp_cmd_pipeline_barrier, OP_vkp_cmd_copy_buffer_to_image, OP_vkp_cmd_copy_image_to_buffer, etc. β€” command recording that needs ht_get on the dispatchable cmdbuf handle (codegen doesn't know which arg is dispatchable).
  • OP_vkp_get_buffer_device_addr, OP_vkp_get_phys_mem_props, and other "spoofable" property queries.

How a call flows

Take vkCmdBindIndexBuffer as the worked example:

  1. The Vulkan loader inside the client process loads libvulkan.so.1, scans /usr/local/share/vulkan/icd.d/, finds mali_proxy_icd.json, dlopens mali_proxy_icd.so, then calls vk_icdGetInstanceProcAddr (in icd_entry.c) for every entry point. icd_entry.c calls _vkproxy_lookup, which walks the codegen'd table in stubs_gen.c. For overrides that need hand work, manual_stubs.c's _vkp_manual_override is consulted first inside the ICD's own vkGetDeviceProcAddr / vkGetInstanceProcAddr.
  2. The client records its command-buffer call. The function pointer it got is one of these:
    • the codegen stub in stubs_gen.c β€” marshals scalar args, sends the opcode via vkp_send_cmd (fire-and-forget), no reply.
    • a manual implementation in manual_stubs.c β€” same protocol, but handles things gen.py doesn't (variable-length arrays, pNext chains, dispatchable-handle translation, ABI mismatches between spec versions).
  3. vkp_send_cmd (in client/transport.c) takes the global socket lock, writes vkp_cmd_hdr + payload to the socket, and returns. For queries that need a reply, vkp_call blocks until the server replies.
  4. husky_vkserver's per-client thread reads the header, grows the payload buffer if needed, then dispatches:
    • opcode < VKP_OP_BASE_SYNTH (0x8000): try vkp_dispatch_manual first (it owns a fixed list of opcodes that need handle translation), then fall back to the codegen dispatcher vkp_dispatch in dispatch_gen.c.
    • opcode β‰₯ 0x8000: vkp_dispatch_manual only (handles all OP_vkp_*).
  5. The chosen handler unmarshals into stack-allocated Vulkan structs and calls g_pfn.vkCmdBindIndexBuffer β€” a real pointer obtained at bring-up by walking the driver's GetInstanceProcAddr. The handle table (handle_table.c) translates the client-side cookie for VkCommandBuffer into the driver's actual dispatchable handle.
  6. For commands that produce a reply (vkAllocateMemory, vkCreateBuffer, the vkGet*Properties family), the handler calls vkp_send_reply to ship the returned VkResult + result struct back to the client.

Codegen (codegen/gen.py)

Driven from upstream vk.xml. Walks every <command> element and classifies its args:

  • Scalars / by-value handles β†’ emit a 1:1 marshal into a packed struct.
  • Pointers to small fixed-size structs β†’ emit a struct-copy.
  • Anything else (variable-length arrays, pNext chains, ambiguous unions, dispatchable handles, sType-based polymorphism) β†’ emit a vkp_not_implemented stub. Those commands are picked up by manual_stubs.c / dispatch_manual.c and registered in _vkp_manual_override so the lookup chain finds the hand-written version first.

Outputs:

  • include/vkproxy_proto_gen.h β€” enum vkp_opcodes { OP_vkXxx = N, ... }
  • client/stubs_gen.c β€” every VKAPI_ATTR ... VKAPI_CALL vkXxx(...) plus a name β†’ fn-pointer table _vkp_stubs[] consulted by _vkproxy_lookup.
  • server/dispatch_gen.c β€” vkp_dispatch() is one big switch over opcodes that unmarshals and calls g_pfn.vkXxx.
  • server/vk_funcs_gen.c/.h β€” struct vk_funcs g_pfn { PFN_vk...; } plus a populator that calls GetInstanceProcAddr for every member.

Regenerate: python3 codegen/gen.py.

Build

make                # builds husky_vkserver (Android Bionic), mali_proxy_icd.so (glibc aarch64),
                    # and vkp_hello_test (smoke client). Output in build/.

make pc             # builds husky_vkserver_pc (host x86_64 glibc) against a runtime-dlopen'd
                    # libvulkan.so.1. No ue-husky link β€” texture substitution is Mali-only.

The Makefile splits compilers:

  • CC_AND = aarch64-linux-android29-clang from $NDK/toolchains/... for the Bionic server.
  • CC_ARM64 = aarch64-linux-gnu-gcc for the glibc ICD and smoke client.
  • CC_HOST = gcc for the host-side PC build.

The ICD links with -Wl,-Bsymbolic so internal references to vk* symbols bind to the ICD's own copies; otherwise the Vulkan loader (which is loaded first into the process) would interpose, cause infinite recursion, and reject the ICD.

Deploy

Target Path on the GPU-side machine
husky_vkserver /var/lib/machines/halium/data/local/tmp/husky_vkserver (runs in chroot)
libue_husky.so (sibling) same dir, beside husky_vkserver (rpath $ORIGIN)
mali_proxy_icd.so /usr/local/lib/vkproxy/mali_proxy_icd.so
mali_proxy_icd.json /usr/local/share/vulkan/icd.d/mali_proxy_icd.json

The server is run as a systemd unit (husky-vkserver.service). The unit's ExecStart chroot-execs it and pipes stderr into /tmp/vksrv.log inside the chroot.

A Vulkan client process picks up the ICD via:

export VK_ICD_FILENAMES="/usr/local/share/vulkan/icd.d/mali_proxy_icd.json"
export VK_LOADER_DRIVERS_SELECT="mali_proxy_icd.json"

Runtime knobs

Variable Side Effect
VKPROXY_SOCKET server UDS path (default /tmp/husky-vk.sock)
VKPROXY_LISTEN PC host:port for the TCP variant
VKPROXY_REMOTE client If set, dial TCP instead of UDS
UEHUSKY_VERBOSE server Turn on verbose op-by-op logging
VKPROXY_SPOOF_SM6 client Spoof shaderInt64/Float64/Int16 (for SM6 / D3D12). Off by default.
VK_LOADER_DEBUG=all loader Khronos loader trace; useful for "why didn't this ICD load"

Debugging

husky_vkserver's SIGSEGV/SIGABRT/SIGBUS/SIGILL handler (main.c):

  1. Writes *** FATAL SIGNAL *** and sig=N tid=T code=K addr=A pc=P.
  2. Walks the aarch64 frame-pointer chain via the ucontext_t and prints up to 20 caller PCs.
  3. Dumps /proc/self/maps.
  4. Re-raises with SIG_DFL so systemd still sees a non-zero exit.

To resolve a PC to a library:offset, find the maps line whose VA range covers the PC, then RVA_in_file = pc - va_start + file_offset. Open the .so in IDA at its ImageBase and jump to that RVA.

Known sharp edges (Mali backend)

These apply only when the back-end driver is Mali's Valhall blob. The proxy itself is driver-agnostic; everything below is upstream-driver behaviour worked around in dispatch_manual.c or the kernel module.

  • Mali user-space race: libGLES_mali.so:sub_1A65300 walks a registry inside a pthread_mutex_t it doesn't itself create. Two concurrent server threads can observe a partially-NULL'd registry pointer and dereference NULL + 0xC8. Mitigation: serialise all dispatch through a single mutex in main.c. Lower throughput, no crash.
  • Mali kbase CONFIG_MALI_DMA_BUF_MAP_ON_DEMAND=y (kernel side): imports do not eager-map dma_bufs into the GPU page table; the fault handler refuses to demand-map and kills the context. Patch the kernel module to #undef CONFIG_MALI_DMA_BUF_MAP_ON_DEMAND in mali_kbase_mem.c and mali_kbase_mem_linux.c.
  • -Wl,-Bsymbolic is mandatory on the ICD or the loader rejects it with an infinite-recursion error.
  • GS/tess on Mali-G715: advertised but draw-time hangs the MCU. Strip in vkCreateDevice feature filtering on the server.

About

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

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors