Skip to content

Latest commit

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date

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:

bitnamemeaning
0VKP_FLAG_EXPECTS_REPLYclient wants a synchronous reply (used by vkp_call)
1VKP_FLAG_REPLYthe frame is a reply (server -> client direction)
2VKP_FLAG_HAS_FDone fd is being passed via SCM_RIGHTS on this datagram

Opcode space:

RangeMeaning
0x0000 – 0x7FFFOne-to-one mirror of a real Vulkan command — generated by gen.py
0x8000 – 0xFFFFSynthesised / 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.henum 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.cvkp_dispatch() is one big switch over opcodes that unmarshals and calls g_pfn.vkXxx.
  • server/vk_funcs_gen.c/.hstruct 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

TargetPath 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

VariableSideEffect
VKPROXY_SOCKETserverUDS path (default /tmp/husky-vk.sock)
VKPROXY_LISTENPChost:port for the TCP variant
VKPROXY_REMOTEclientIf set, dial TCP instead of UDS
UEHUSKY_VERBOSEserverTurn on verbose op-by-op logging
VKPROXY_SPOOF_SM6clientSpoof shaderInt64/Float64/Int16 (for SM6 / D3D12). Off by default.
VK_LOADER_DEBUG=allloaderKhronos 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

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages