Skip to content

[WIP] gallium/va: add termux-va bridge for hardware VA-API decode via the Termux MediaCodec daemon - #100

Draft
lfdevs wants to merge 7 commits into
dev/adreno-mainfrom
test/add-va-bridge
Draft

[WIP] gallium/va: add termux-va bridge for hardware VA-API decode via the Termux MediaCodec daemon#100
lfdevs wants to merge 7 commits into
dev/adreno-mainfrom
test/add-va-bridge

Conversation

@lfdevs

@lfdevs lfdevs commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Summary

Based on DroidSpaces Media Decode Daemon by Re-s, this branch adds a termux-va bridge to the VA frontend: VA-API video decoding is forwarded over a Unix socket to the termux-va daemon running in Termux, which decodes with the Android MediaCodec API in hardware and returns NV12 frames. Applications inside a Linux container that shares Termux's tmp directory (proot-distro ... --shared-tmp) get hardware decoding through the standard VA-API without any modification — ffmpeg, Firefox and Chrome work unmodified.

This follows the porting model established by anland-termux: a Termux daemon, a Unix socket placed in the shared tmp directory, and a bridge on the container side. The wire protocol is byte-compatible with droidspaces-media-decode protocol v3 (HELLO_MAGIC 0x444D4400), which lets us reuse its protocol regression tooling.

The daemon itself lives in the companion repository lfdevs/termux-va (NDK r29, 29.0.14206865, same pin as anland-termux); it listens on $TMPDIR/termux-va/termux-va.sock, which appears as /tmp/termux-va/termux-va.sock inside the container.

How it works

The 26.x VA frontend already parses VA buffers and prepends H.264/HEVC start codes to slice data, so the bridge is thin:

  • Wrapped screen (tva_bridge.c): get_video_param / is_video_format_supported advertise the bridge codec set; everything else forwards to the real screen.
  • Wrapped multimedia context: create_video_codec hands bitstream decode to the bridge; create_video_buffer(_with_modifiers) falls back to the generic vl_video_buffer_create helper, because the underlying drivers have no video path (freedreno/llvmpipe).
  • Codec: decode_bitstream accumulates the frontend's slice data; end_frame splits it into Annex B units (one NALU per daemon length prefix, parameter sets excluded from the unit index exactly like the daemon), sends them and returns — the pending pipeline depth is capped at 6 to stay within the daemon's 8-slot SHM pool.
  • fence_wait (called by vaSyncSurface): frames are pumped into staging buffers, paired with the picture by the daemon's unit index, and the visible (cropped) region is copied into the surface's plane resources on the caller's thread, honoring the decoder's stride/slice-height geometry (Venus aligns buffers to 128x32).

Everything runs on the application thread — no bridge-owned threads touch pipe_context. Frames come back inline on the socket or zero-copy through a memfd slot pool handed over via SCM_RIGHTS (the daemon-side copy from the gralloc output buffer remains, as upstream).

Codec support and known limitations

  • Supported: H.264 (Constrained Baseline / Main / High), HEVC Main, VP9 Profile 0 — NV12 progressive output only.
  • Encode and other profiles are not provided (queries fall through to the real screen).
  • vaDeriveImage is not available (plane resources are separate textures, SUPPORTS_CONTIGUOUS_PLANES_MAP is not claimed); CPU consumers can use vaGetImage.
  • The default GPU screen comes from the application's own display connection (e.g. the render node with freedreno-kgsl), so vaExportSurfaceHandle depends on the underlying screen's dma-buf export; decode itself never touches the GPU.

Activation (runtime-gated)

A build with the bridge behaves exactly like an unmodified one until activated:

  • TERMUX_VA_BRIDGE=1 forces it on, 0 forces it off.
  • Unset: activates when TERMUX_VA_SOCKET / TERMUX_VA_SOCKET_DIR is set, or when the default endpoint exists as a socket.
  • Endpoint resolution: TERMUX_VA_SOCKET (full path) > TERMUX_VA_SOCKET_DIR (directory) > /tmp/termux-va/termux-va.sock, read through os_get_option so Android system properties work as a fallback. The same variables are understood by the daemon.
  • Select the driver with LIBVA_DRIVER_NAME=termuxva (the megadriver is additionally exposed as termuxva_drv_video.so).
  • If the daemon is unreachable, driver init fails cleanly and applications fall back to software decoding.

Build & CI

  • New meson feature option termux-va-bridge (default auto). The bridge sources are compiled into libva_st unless disabled; the option also relaxes the VA state tracker's gallium-driver whitelist so a freedreno-only build can enable gallium-va for the bridge.
  • Requires h264dec/h265dec/vp9dec in video-codecs (e.g. -Dvideo-codecs=all) for the frontend's parsers.
  • New docs: docs/termux-va.rst (index-linked) and the bridge's environment variables in docs/envvars.rst.

lfdevs added 4 commits August 29, 2026 20:07
tva_protocol.h is the cmp-verified mirror of the daemon's
common/tva_protocol.h (repository lfdevs/termux-va); the two copies must
stay byte-identical (scripts/check-mirror.sh in the daemon repo).

tva_client.{h,c} is a port of droidspaces-media-decode's
vaapi-driver/src/dmd_client.{c,h} (Apache-2.0, relicensed GPL-3.0 with
the modification notice in the file headers).  Changes vs upstream:
Unix-socket-only transport (TCP removed, wire values unchanged),
protocol constants from the mirror header, symbols renamed dmd_* ->
tva_*, and the default endpoint resolves in container view
(TERMUX_VA_SOCKET > TERMUX_VA_SOCKET_DIR > shared-tmp paths, i.e.
/tmp/termux-va/termux-va.sock) through Mesa's os_get_option so Android
system properties work as a fallback.

Kept faithful: the v3 handshake with the endpoint dev/ino reconciliation,
the v2 downgrade retry, the SHM pickup through the abstract socket with
SCM_RIGHTS, the format-block parsing with CAP_FRAME_PTS, non-blocking I/O
with bounded waits, MSG_NOSIGNAL and CLOEXEC everywhere.
The bridge delegates VA-API bitstream decode to the termux-va daemon
(MediaCodec hardware decode in Termux) over the Unix socket described by
tva_client.h, and runs entirely on the application thread - no
bridge-owned threads touch pipe_context.

- tva_screen wraps the real screen: get_video_param /
  is_video_format_supported advertise the bridge codec set
  (H.264 Baseline/Main/High, HEVC Main, VP9 Profile0, NV12 only,
  96x96..8192x4320, progressive) while everything else forwards.
- tva_pipe wraps the multimedia context: create_video_codec hands decode
  to the bridge codec, and create_video_buffer(_with_modifiers) falls
  back to the generic vl_video_buffer_create helper because the
  underlying drivers have no video path.
- The codec accumulates the frontend's slice data (which already carries
  H.264/HEVC start codes; parameter sets arrive as slice data buffers),
  splits it into Annex B units in end_frame (parameter sets excluded
  from the unit index exactly like the daemon), and pairs the returned
  frame with the picture by the daemon's unit index through a pending
  ring whose depth is capped by the daemon's SHM slot pool.
- fence_wait pumps frames into staging buffers and copies the visible
  (cropped) region of the NV12 buffer into the surface plane resources
  on the caller's thread, honoring the decoder stride/slice_height
  geometry from the format block.

Derived in part from droidspaces-media-decode's vaapi-driver (Apache-2.0):
the capability table, pipeline-depth model and unit classification; the
modification notice per GPL-3.0 section 5 is in the file header.
- VA_DRIVER_INIT_FUNC wraps the multimedia context with the bridge right
  after it is created (activation is runtime-gated by the TERMUX_VA_*
  environment variables) and appends a marker to the vendor string so
  vainfo shows when the bridge is active.
- New meson feature option 'termux-va-bridge' (default auto): the bridge
  sources are compiled into libva_st unless disabled, and the option also
  relaxes the VA state tracker's gallium-driver whitelist so a
  freedreno-only build can enable gallium-va for the bridge.
- targets/dri exposes the megadriver as termuxva_drv_video.so so libva
  can dlopen it via LIBVA_DRIVER_NAME=termuxva.

Runtime activation summary: TERMUX_VA_BRIDGE=1 forces the bridge on,
=0 forces it off; with the variable unset the bridge activates when
TERMUX_VA_SOCKET/TERMUX_VA_SOCKET_DIR is set or the default endpoint
/tmp/termux-va/termux-va.sock exists.  A failed bridge activation
returns a VA error so applications fall back to software decoding
instead of crashing.
build-check.yml: the debian-trixie job now runs on pushes to
test/add-va-bridge and workflow_dispatch (the other distro jobs stay
pull_request-only; their clone step only knows the PR head ref), installs
libva-dev, and enables the VA bridge in the packaged tar.gz:
-Dgallium-va=enabled -Dtermux-va-bridge=enabled -Dvideo-codecs=all.  The
clone step falls back to the pushed branch when no PR ref exists.

docs: add docs/termux-va.rst (building, activation, socket location, data
path, troubleshooting) to the documentation index and document the
bridge's environment variables (TERMUX_VA_BRIDGE, TERMUX_VA_SOCKET,
TERMUX_VA_SOCKET_DIR, DMD_WANT_SHM, DMD_VA_LOG, LIBVA_DRIVER_NAME) in
docs/envvars.rst.
@lfdevs
lfdevs force-pushed the test/add-va-bridge branch from dc31abe to 893122f Compare August 29, 2026 12:21
lfdevs added 3 commits August 30, 2026 12:52
…ction

The stock loader path cannot create a screen on the kgsl stack: Xiaomi/
DroidSpaces kernels report the display controller's DRM node as
"msm_drm" (no pipe_loader descriptor matches, so kmsro/zink fallbacks
engage and fail - "ZINK: failed to choose pdev" - because the kgsl stack
has no usable Vulkan device), and even a node reported as "msm" drives
no GPU there.  The VA frontend's vscreen creation failed before the
bridge's codec ever came into play.

tva_bridge_vscreen_create() now creates the underlying screen with a
TERMUX_VA_GPU_BACKEND selection:

- auto (default): stock loader selection, then the fork's "kgsl"
  freedreno alias, then llvmpipe over the null sw winsys.
- kgsl: force the "kgsl" alias - pipe_loader re-points driver_name at
  the fork-registered descriptor whose device layer redirects GPU
  submission to /dev/kgsl-3d0 while keeping the handed fd as the
  control/identity fd (freedreno_device.c); FD_FORCE_KGSL=1 is set when
  unset, matching the EGL path (MESA_LOADER_DRIVER_OVERRIDE=kgsl).
- drm: stock selection only.  sw: llvmpipe only.

The frontend's DRM/Wayland vscreen creation calls the bridge when it is
active; the fd stays owned by libva (pipe_loader dups internally).
Install Termux VA capability and codec hooks directly on the real Gallium
screen and multimedia context instead of creating memcpy-based wrapper
objects that can be misinterpreted by native drivers.

Move frame reception to a dedicated reader thread, track pending frames by
input-unit identity, and keep surface copies on the application thread.
Add reversible drain and explicit frame-release handling for inline and SHM
transport.

Support TERMUX_VA_GPU_BACKEND=sw through a null winsys and llvmpipe so the
bridge can operate without KGSL, DRM, or Vulkan access. Initialize and
release the GLSL type singleton in the Freedreno screen lifetime to prevent
shader compilation from using a null GLSL linear allocation context.
Align the Termux VA documentation and bridge comments with the tested backend selection. The auto mode uses the stock DRM loader and falls back to llvmpipe, while KGSL is selected explicitly and sw forces llvmpipe.

Document that the current Mesa bridge advertises H.264 and VP9 Profile 0 only, while HEVC parsing and CSD synthesis remain incomplete.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant