Skip to content

[WIP] gallium/va: add termux-va bridge for hardware VA-API decode & encode - #100

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

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

Conversation

@lfdevs

@lfdevs lfdevs commented Aug 29, 2026 •

Copy link
Copy Markdown
Owner

Tip

See also: Termux:VA

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.

@lfdevs
lfdevs force-pushed the test/add-va-bridge branch 2 times, most recently from bb69993 to 3f29ee1 Compare September 1, 2026 05:36
@lfdevs
lfdevs force-pushed the test/add-va-bridge branch from 76697c0 to a5e3b8f Compare September 9, 2026 03:51
@lfdevs
lfdevs force-pushed the test/add-va-bridge branch from 54919b6 to 09e2cbe Compare October 2, 2026 16:33
@lfdevs lfdevs changed the title [WIP] gallium/va: add termux-va bridge for hardware VA-API decode via the Termux MediaCodec daemon [WIP] gallium/va: add termux-va bridge for hardware VA-API decode & encode Oct 3, 2026
lfdevs added 25 commits October 8, 2026 15:48
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.
…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.
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.
Add TERMUX_VA_DISABLE_AV1 to suppress the AV1 Main profile while
keeping H.264, HEVC, and VP9 available through the bridge.

This lets devices without a hardware AV1 MediaCodec decoder fall back
to the application's native software decoder, avoiding unnecessary
bridge transport and presentation overhead.
Continue submitting hidden VP9 pictures to MediaCodec so they can update
the decoder reference state, but do not reserve output fences for them.

Chromium splits VP9 superframes into hidden and displayed pictures, while
MediaCodec produces no output buffer for hidden pictures. Avoid leaving
permanent holes in the pending queue that eventually cause vaEndPicture
to fail.
Use the fixed DPB size reported by VA-API when synthesizing H.264 PPS reference-list defaults. VA-API does not preserve num_ref_idx_active_override_flag, so per-picture reference counts cannot be promoted to PPS defaults.

Keeping the synthetic PPS stable prevents Qualcomm MediaCodec from resetting mid-stream and exhausting the pending output queue.
Add TERMUX_VA_DISABLE_AVC, TERMUX_VA_DISABLE_HEVC, and TERMUX_VA_DISABLE_VP9 alongside the existing AV1 switch. Accept TERMUX_VA_DISABLE_H264 as an AVC compatibility alias and share the boolean option parsing across all codec switches.

Disabled profiles are hidden from libva so applications can select their native software decoders when a MediaCodec implementation is unavailable, software-only, or being excluded for compatibility testing. Document the switches and their accepted values in the termux-va bridge documentation.
Do not derive synthesized H.264 PPS active-list defaults from the VA-API DPB size. VA-API does not preserve the PPS default-reference fields or the slice override flag, and Bilibili and Chromium streams can advertise a DPB larger than their active L0 list.

Use a stable two-reference L0 and zero-reference L1 default, bounded by the configured DPB, so slices that rely on the original PPS defaults remain valid without changing parameter sets mid-stream. This prevents Qualcomm MediaCodec from rejecting the stream and exhausting the pending output queue.
lfdevs added 8 commits October 8, 2026 15:49
GBM issues DRM_IOCTL_GET_CAP while probing render devices, but the KGSL
character device exposed in PRoot is not a DRM node and returns ENOTTY.
Emulate the minimal capability set needed by the native-pixmap path:
monotonic timestamps and PRIME import/export, while leaving dumb-buffer
and modifier support disabled.

Add opt-in ioctl tracing via TERMUX_VA_DRM_SHIM_LOG to diagnose DRM/GBM
initialization failures without changing default logging.
Recognize explicitly opened /dev/mali0 descriptors and provide the minimal
libdrm metadata needed for VA-API display initialization.

Keep Mali out of global DRM device enumeration so it is not selected as
the graphics render node, while preserving the existing KGSL path.
Advertise AVC Constrained Baseline/Main/High and HEVC Main encode profiles
through the Termux VA bridge, with NV12 input and the device-supported
dimension limits.

Map encode profiles to the new termux-va protocol IDs. Copy NV12 planes into
packed encoder frames, receive encoded access units synchronously, and write
them into VA bitstream buffers with feedback sizes. Keep encoder sessions
separate from the decoder reader-thread and shared-memory paths.

Mirror the updated protocol definitions and preserve the existing decoder
lifecycle synchronization.
PRoot environments without DRM render nodes were implicitly using
non-blocking VA surface synchronization. When FFmpeg combined VAAPI
decode, VPP scaling, and hardware encoding, VPP could sample a decoder
surface before termux-va had copied the frame, producing green initial
frames.

Make non-blocking synchronization opt-in through
DMD_VA_SURFACE_NO_WAIT, and preserve explicit waiting for termux-va
fences consumed by the encoder.
Capture the VA-API encoder target bitrate and frame rate from H.264 and
HEVC picture descriptors, then pass them to the termux-va daemon through
the encoder handshake.

Create encoder sessions lazily after the first picture parameters are
available, ensuring that -b:v settings are applied before MediaCodec is
configured. Extend the client configuration and keep the shared
protocol header synchronized with the termux-va repository.
The llvmpipe null winsys cannot allocate display targets when
PIPE_BIND_SHARED and PIPE_BIND_LINEAR are requested. This caused
Termux VA surface creation to fail in Pixel PRoot containers using
TERMUX_VA_GPU_BACKEND=sw.

Detect the explicit software backend and preserve the original resource
binds in that case. Keep shared linear resources for hardware-backed
backends, where they are required for DMA-BUF export.
Advertise VAProfileVP9Profile0 with VAEntrypointEncSlice and map VP9
encoding requests to the termux-va VP9 encoder protocol ID.

Preserve VP9 bitrate and frame-rate parameters, retain the VA coded-buffer
handle, and allocate a staging resource so encoded packets remain available
through VAMapBuffer.
Export the requested Freedreno resource plane instead of reusing the
luma resource for chroma. Align KGSL NV12 plane pitches consistently and
round subsampled dimensions up for small capability-probe allocations.

Configure encoder sessions from the actual source buffer dimensions
rather than macroblock-aligned context dimensions. Forward explicit AVC
Baseline, Main and High profile requests using the mirrored protocol
extensions.

Drain pending AVC and HEVC decoder output during context destruction
and complete retained frame copies on the application thread within a
shared timeout. Preserve reader-thread restrictions and teardown lock
ordering so exported frames contain the final decoded pixels.

Document Chromium encoding requirements and frame-rate limitations.
@lfdevs
lfdevs force-pushed the test/add-va-bridge branch from c7c0264 to 7573b8c Compare October 8, 2026 07:49
lfdevs added 8 commits October 8, 2026 20:19
A resource's backing storage can change while its sampler view remains
bound. Looking up texture state by the unchanged view ID can then reuse
stale descriptors and trigger the view_rsc_seqno assertion, aborting
XWayland during VLC playback.

Update bound sampler views before constructing the cache key. The
existing helper invalidates stale entries and refreshes descriptors,
while retaining its fast path when the resource sequence is unchanged.

Also index the supplied sampler-view array from zero rather than adding
the destination binding offset, consistent with fd_set_sampler_views().
Resolve the EGL device from the KGSL render descriptor before applying
the existing software fallback for KGSL and Zink displays.

This allows the Wayland DRM shim to expose /dev/kgsl-3d0 as an
accelerated EGL device, so applications such as Firefox can report an
accelerated renderer and enable VA-API hardware video decoding.

Keep the new lookup guarded by HAVE_LIBDRM and retain the existing
software-device behavior when no matching DRM device is available.
VA-API does not expose several AV1 sequence-level fields required when
reconstructing a bitstream for Android MediaCodec. Keep the generated
sequence header self-consistent by disabling loop restoration and omitting
qm_v when separate_uv_delta_q is forced to zero.

Emit allow_warped_motion for every non-intra, non-error-resilient frame as
specified by AV1. This field is independent of is_motion_mode_switchable;
omitting it shifted the tile-group header in multi-tile frames.

Tested on a Pixel 6 Pro Debian PRoot container with FFmpeg and termux-va:
all eight AV1 samples from 256x128 through 3840x1920 decoded 240 frames
successfully. Hardware NV12 frame checksums for multi-tile samples matched
software dav1d decoding.
Pixel MediaCodec can retain the final AV1 showable frame until an EOS
signal. Since VA-API has no explicit decoder flush callback, the bridge
could leave the final temporal unit pending indefinitely and make FFmpeg
wait forever.

Flush the terminal AV1 unit after a configurable idle period, send a
serialized reversible drain request, and serialize bridge socket writes.
Also release the context mutex before blocking fence waits to avoid a
driver/context lock inversion in surface and buffer synchronization.
Pixel C2 AV1 streams expose incomplete sequence and frame syntax through
the VA-API descriptors. Reconstruct the missing screen-content, restoration,
initial-display-delay, skip-mode, and warped-motion fields from the available
descriptor state and tested Pixel stream variants.

Carry restoration state through the bridge, correct color/chroma and loop
restoration bit layout, restore reduced transforms for affected 1080p base
frames, and serialize hidden-frame show_existing units to preserve reference
and display order.
The MediaCodec bridge could submit access units before synthesized
parameter sets and reference state were complete. On Pixel 6 Pro, this
caused H.264 streams with larger reference lists to stall and could
reorder or drop HEVC frames.

Parse and retain HEVC short-term RPS indices, reconstruct reference
picture sets, and track temporal sublayers while generating codec
configuration data. Queue initial H.264 and HEVC access units until the
reference state is known, then flush them with ordered sequence numbers,
retained surfaces, and fences. Also derive H.264 SPS/PPS defaults from
the VA descriptors and release queued resources during teardown.
The AV1 bridge always encoded non-final tile sizes as two-byte values,
although the frame header may require up to four bytes. Large Pixel C2
frames could therefore become misaligned after the first tile.

Derive the tile-size width from the largest non-final tile and use it
consistently in the frame header and tile group. Also avoid treating the
BT.709 matrix coefficient as a universal Pixel warped-motion marker,
while retaining the validated 1080p compatibility path.
Android MediaCodec decoders may retain the final reordered H.264 or HEVC
picture after the last access unit is submitted. Since VA-API has no decoder
flush callback, the terminal fence could wait indefinitely during conversion
teardown.

When the newest pending AVC or HEVC unit remains idle, request a reversible
daemon drain to release the delayed picture. Keep the idle timeout
configurable through DMD_REORDER_DRAIN_IDLE_MS, defaulting to five seconds.
Avoid draining older pending units while decoding is still progressing, and
fail the terminal fence after a second timeout if the decoder still produces
no matching output.
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