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 20 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 20 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 4 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.
Synchronize the bridge reader and fence lifecycle,
retain pending surface resources, and make teardown
cancel the socket worker safely. Export KGSL bridge
surfaces as linear DMA-BUF-backed resources with
complete object metadata.

The bridge now owns video capability queries,
initializes KGSL DMA-BUF allocation before screen
creation, and avoids advertising unsupported external
timeline semaphore FDs to Vulkan clients on KGSL.
This lets FFmpeg and ffplay consume exported VA
surfaces while preserving the existing Unix-socket
protocol.
@lfdevs
lfdevs force-pushed the test/add-va-bridge branch from bb69993 to 3f29ee1 Compare September 1, 2026 05:36
Keep complete H.264/HEVC access units together, retain pending resources until frame copies complete, and make CPU/Gallium uploads explicit for KGSL. Synchronize imported DMA-BUFs before Turnip use, add cache and barrier diagnostics, and document the pipeline and copy controls.
Synchronize imported dma-bufs across KGSL producer and consumer contexts, invalidate shared NV12 texture state, and retain pending VA frames until MediaCodec output arrives. Improve the lowered NV12 import path and synthesize H.264 parameter sets without an incorrect zero-reorder restriction while restoring reference defaults from the decoder DPB.
Preserve the VA H.264 profile when rebuilding SPS headers and emit the High-profile SPS and PPS fields required by MediaCodec. Keep zero PPS reference defaults for High streams while retaining the DPB fallback for the existing Main-profile path.
Use the active reference-list sizes reported by VA slice parameters when synthesizing Main-profile PPS headers. Keep the largest observed values in the codec so CSD updates converge during the first GOP instead of being regenerated for every frame.
Advertise HEVC Main through the termux-va bridge and synthesize the VPS, SPS, and PPS parameter sets required by MediaCodec from VA-API picture descriptors. Forward HEVC slice data unchanged and cache parameter sets so Chrome and FFmpeg can use the hardware decoder.

Reject parameter sets whose reference-picture-set contents are not exposed by VA-API instead of sending malformed CSD.
Keep the short-term reference picture set count when synthesizing HEVC SPS headers. Qualcomm's decoder requires a non-zero SPS RPS count even when FFmpeg supplies each slice's RPS inline, while VA-API does not expose the set contents, so emit empty placeholders and reject slices that refer to unavailable SPS RPS data.
Advertise AV1 Main and route it through the termux-va codec. Reconstruct
complete AV1 temporal units from VA-API picture and tile descriptors,
including sequence headers, frame OBUs, tile groups, hidden reference frames,
and refresh masks required by MediaCodec.

Add shared H.264/HEVC bitstream helpers and AV1 OBU writers, and include the
new sources in the VA frontend build.
Handle hidden AV1 pictures as output frames or synthesize
show_existing_frame OBUs so MediaCodec returns reference surfaces needed by
the VA bridge. Track per-picture pending entries and wait for recycled fences
before detaching them, preventing late frames from overwriting surfaces reused
by Chromium.

Copy decoded linear resources directly into exported DMA-BUFs from the reader
thread when possible, retain the Gallium transfer fallback, and avoid
unnecessary flushes for CPU copies. Add configurable AV1 output modes, pending
limits, fence wait timing, and tracing for diagnostics.
VAAPI does not expose H.264 PPS default reference-list counts. Infer them
from effective slice parameters when synthesizing CSD, while keeping learned
High-profile defaults stable across subsequent slices.

This prevents Qualcomm MediaCodec from stalling on High@L4 streams after the
initial IDR frames.
Open /dev/kgsl-3d0 when a VA display has no DRM fd and route screen
creation through the KGSL Freedreno alias. Automatically allocate
linear NV12 surfaces from a single dma-buf in KGSL-only containers,
while preserving per-plane offsets for PRIME exports. Treat shared
plane resources as one exported object when PRoot cannot compare file
descriptions.

Document the PRoot XWayland requirements and the contiguous dma-buf
override.
Support Chromium hardware video decode in PRoot containers that expose
/dev/kgsl-3d0 but no usable DRM render node.

- Synchronize KGSL dma-buf cache ownership and imported resource handling.
- Stabilize VA bridge staging, fence retirement, surface teardown, and VPP
  synchronization.
- Support linear NV12 exports and planar DRI/GBM imports used by Chromium.
- Make Freedreno compositor paths compatible with KGSL.
- Propagate TERMUX_VA_GPU_BACKEND=kgsl through the loader, X11 DRI3, EGL,
  and GLX paths.
- Document the DRM-less PRoot configuration and add bridge diagnostics.
Add the optional termux-va-wayland-shim Meson feature and build
libtva_drm_shim_wayland.so for Chromium's native Wayland backend in
DRM-less KGSL PRoot containers.

The shim redirects Chromium's DRM discovery and version probes to
/dev/kgsl-3d0 without replacing Mesa's KGSL backend. Enable the target
in build-check workflows and document the required LD_PRELOAD launch
configuration.
@lfdevs
lfdevs force-pushed the test/add-va-bridge branch from 76697c0 to a5e3b8f Compare September 9, 2026 03:51
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.

2 participants