pax_global_header00006660000000000000000000000064152676464260014534gustar00rootroot0000000000000052 comment=51df9a538a1e8ce7a02449d9bca32ecf621c76c5 qcom-va-driver-1.1.0/000077500000000000000000000000001526764642600143675ustar00rootroot00000000000000qcom-va-driver-1.1.0/.clang-format000066400000000000000000000004161526764642600167430ustar00rootroot00000000000000BasedOnStyle: LLVM IndentWidth: 4 ColumnLimit: 100 PointerAlignment: Right DerivePointerAlignment: false BreakBeforeBraces: Attach AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: Never AllowShortLoopsOnASingleLine: false SortIncludes: Never qcom-va-driver-1.1.0/.editorconfig000066400000000000000000000004231526764642600170430ustar00rootroot00000000000000root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true indent_style = space indent_size = 4 [{meson.build,meson_options.txt,*.yml,*.yaml,PKGBUILD,.clang-format}] indent_size = 2 [*.md] trim_trailing_whitespace = false qcom-va-driver-1.1.0/.github/000077500000000000000000000000001526764642600157275ustar00rootroot00000000000000qcom-va-driver-1.1.0/.github/workflows/000077500000000000000000000000001526764642600177645ustar00rootroot00000000000000qcom-va-driver-1.1.0/.github/workflows/debian.yml000066400000000000000000000075501526764642600217400ustar00rootroot00000000000000name: Debian packages on: push: branches: ['**'] tags: ['v*'] pull_request: workflow_dispatch: permissions: contents: read jobs: build: runs-on: ubuntu-24.04-arm container: ubuntu:noble env: DEBIAN_FRONTEND: noninteractive steps: - name: Install packaging tools run: | apt-get update apt-get install -y --no-install-recommends ca-certificates git build-essential devscripts equivs lintian - uses: actions/checkout@v4 - name: Install build dependencies run: mk-build-deps --install --remove --tool 'apt-get -y --no-install-recommends' debian/control - name: Check GPU-copy build options run: python3 tests/build/gpu-copy.py tmp/build-options - name: Build binary packages run: | version=$(dpkg-parsechangelog -S Version) if [ "$GITHUB_REF_TYPE" = tag ]; then case "$GITHUB_REF_NAME" in "v$version"|"v${version%-*}") ;; *) echo 'Release tag does not match the package version' >&2; exit 1 ;; esac fi dpkg-buildpackage -us -uc -b --changes-option=-DDistribution=noble mkdir -p artifacts for artifact in ../*.deb ../*.ddeb ../*.buildinfo ../*.changes; do if [ -f "$artifact" ]; then cp "$artifact" artifacts/ fi done - name: Inspect packages run: | lintian --fail-on error artifacts/*.changes dpkg-deb --contents artifacts/qcom-va-driver_*.deb - name: Generate Radxa repository configuration shell: python3 {0} run: | import json from pathlib import Path releases = [ f'sc8280xp-{suite}{suffix}' for suite in ('trixie', 'noble', 'resolute') for suffix in ('', '-test') ] Path('artifacts/pkg.conf').write_text( json.dumps({'*': {'Releases': releases}}, indent=2) + '\n') - uses: actions/upload-artifact@v4 with: name: qcom-va-driver-arm64 path: artifacts/* if-no-files-found: error validate: needs: build runs-on: ubuntu-24.04-arm strategy: fail-fast: false matrix: image: [debian:trixie, ubuntu:noble, ubuntu:resolute] container: ${{ matrix.image }} env: DEBIAN_FRONTEND: noninteractive steps: - name: Install validation tools run: | apt-get update apt-get install -y --no-install-recommends ca-certificates git python3 libva2 - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: name: qcom-va-driver-arm64 path: artifacts - name: Install and load the Noble-built driver run: | apt-get install -y ./artifacts/qcom-va-driver_*.deb test "$(readlink /usr/lib/aarch64-linux-gnu/dri/msm_drv_video.so)" = v4l2_drv_video.so cmp packaging/environment/60-libva-v4l2.conf /usr/lib/environment.d/60-libva-v4l2.conf cmp packaging/environment/libva-v4l2.sh /etc/profile.d/libva-v4l2.sh python3 - <<'PYTHON' import ctypes import os ctypes.CDLL('libva.so.2', mode=os.RTLD_NOW | os.RTLD_GLOBAL) driver = ctypes.CDLL('/usr/lib/aarch64-linux-gnu/dri/msm_drv_video.so', mode=os.RTLD_NOW) getattr(driver, '__vaDriverInit_1_0') print('Driver loaded and VA-API entry point resolved successfully.') PYTHON release: if: startsWith(github.ref, 'refs/tags/v') needs: [build, validate] runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/download-artifact@v4 with: name: qcom-va-driver-arm64 path: artifacts - uses: softprops/action-gh-release@v2 with: draft: true files: artifacts/* qcom-va-driver-1.1.0/.gitignore000066400000000000000000000013461526764642600163630ustar00rootroot00000000000000# Local notes, diagnostic tools, test data, and historical results. /tmp/ /results/ # Build and development artifacts. /build/ /build-*/ /subprojects/packagecache/ /compile_commands.json /.cache/ __pycache__/ *.py[cod] # External test frameworks. /fluster/ /vaapi-fits/ /test-env/ # Local Arch Linux source archives and package builds. /packaging/arch/src/ /packaging/arch/pkg/ /packaging/arch/*.tar.gz /packaging/arch/*.pkg.tar.zst # Debian packaging artifacts. /obj-*/ /debian/.debhelper/ /debian/qcom-va-driver/ /debian/*.debhelper /debian/*.substvars /debian/files /artifacts/ /*-build-deps_*.deb # Extracted Meson dependencies. /subprojects/QAIC-*/ /subprojects/fastrpc-*/ /subprojects/hexagon-open-access-*/ /subprojects/.wraplock qcom-va-driver-1.1.0/LICENSE000066400000000000000000000024321526764642600153750ustar00rootroot00000000000000MIT License Copyright (c) 2026 libva-v4l2 contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. The patches/ directory modifies Linux kernel files and follows their original GPL-2.0-only licensing instead of this MIT license. Third-party test corpora and frameworks are not included and retain their respective licenses. qcom-va-driver-1.1.0/README.md000066400000000000000000000176721526764642600156630ustar00rootroot00000000000000# qcom-va-driver Experimental VA-API backend for Qualcomm Iris via Linux V4L2 stateful M2M, providing hardware decoding and encoding on **SC8280XP**, tested on **Radxa Dragon Q8B**. AV1 decoding is also tested on **X1E80100**. This is not a generic backend for all V4L2 devices. ## Support | Codec | Decoding | Encoding | | --- | --- | --- | | H.264 / AVC | Constrained Baseline, Main, High (8-bit) | Same profiles (8-bit) | | H.265 / HEVC | Main, Main10 (8/10-bit) | Main (8-bit) | | VP9 | Profiles 0 and 2 (8/10-bit) | — | | AV1 | Main (8/10-bit), when exposed by Iris | — | - Progressive 4:2:0 video only. Decoding tested with FFmpeg, GStreamer, mpv, VLC, Chromium, and Kodi; encoding with FFmpeg, GStreamer and Sunshine. - AV1: tested with FFmpeg and GStreamer from 720p to 4K; no film grain or large-scale tiles. Full reference maps can require an extra hidden decode because VA-API omits AV1 reference refresh flags. X1E80100 tested with Iris firmware `vfw-3.1:rel00101` from Windows qcdx8380 driver version 31.0.174.0; the older tested firmware had intermittent decoder startup failures, including with direct V4L2. - Encoding: CQP/CBR/VBR, even dimensions from 128×128 to 3840×2160, I/P frames with one reference; no B frames. I-frame requests produce IDR frames. HEVC uses Main tier; Main10 encoding is unsupported. - Compatible DMA-BUF paths avoid raw-frame copies; cached surfaces may need GPU or CPU copying. PRIME1/PRIME2 import accepts single-buffer linear NV12/P010/BGRA/RGBA; export uses PRIME2. - Optional CDSP/Vulkan VPP: NV12/P010 scaling, cropping, rotation, bit-depth conversion and RGB overlays with DMA-BUF zero-copy. - Vulkan also supports HDR10 P010 to SDR NV12 tone mapping. - Identified decode errors produce gray frames with the Iris error-timestamp patch below; stalls remain errors. ## Performance HEVC Main (8-bit) average FPS on **Radxa Dragon Q8B — SC8280XP, Iris HFI Gen2**, using FFmpeg 9.0.1 and GStreamer 1.28.6. | Pipeline | Operation | 720p (1280×720) | 1080p (1920×1080) | 4K (3840×2160) | | --- | --- | ---: | ---: | ---: | | FFmpeg VA-API | Decode | 1111 | 806 | 297 | | FFmpeg VA-API | Encode | 1289 | 714 | 206 | | GStreamer VA | Decode | 1088 | 823 | 278 | | GStreamer VA | Encode | 1285 | 709 | 206 | | GStreamer direct V4L2 | Decode | 1199 | 882 | 330 | | GStreamer direct V4L2 | Encode | 1287 | 724 | 210 | Synthetic I/P streams, GOP 60, VBR at 5/10/20 Mbps respectively. Encoding includes CPU NV12 upload; FFmpeg uses **`async_depth=4`**. Decoding excludes CPU download and rendering. Three-run averages, except 4K encoding (one run); actual performance depends on content and pipeline. ## Requirements - Linux with Qualcomm Iris stateful and MSM DRM drivers, plus access to the Iris video devices and DRM render node. - Kernel changes in [patches/](patches/), unless already present: decode-order output, larger CAPTURE pools, encoder deblocking controls, error timestamps, and NOSHOW buffer recycling (required for AV1). - C++17 compiler, Meson ≥ 0.61, Ninja, pkg-config, libva ≥ 1.10 and libdrm development libraries. GPU-copy also requires EGL, GLES 3.2, and GBM. - CDSP VPP is enabled by default and requires a Linux ARM64 build machine, GHC and `syb`. Use `-Dcdsp_vpp=disabled` to skip its dependencies and downloads. ## Build and install The project and distribution package were previously named `libva-v4l2`. The `v4l2` driver name, automatic `msm` alias, and existing environment configuration filenames are retained for compatibility. On Arch Linux: ```sh sudo pacman -S --needed base-devel meson ninja pkgconf libva libdrm mesa libglvnd python3 packaging/make-dist.py cd packaging/arch makepkg -si ``` Regenerate the archive after editing packaged sources. Alternatively, use Meson: ```sh meson setup build --buildtype=release --prefix=/usr -Dlibdir=lib meson compile -C build sudo meson install -C build ``` - `-Dgpu_copy=auto` (default): enable GPU copying when dependencies are available; `disabled` uses CPU copying when needed, `enabled` requires dependencies. DMA-BUF zero-copy paths are unaffected. Distribution packages enable GPU-copy. Adjust `libdir` if drivers belong somewhere other than `/usr/lib/dri`. The installed MSM alias selects this backend automatically on the target platform. Check installation with `vainfo` (`libva-utils` on Arch): ```sh vainfo --display drm --device /dev/dri/renderD128 ``` ## CDSP video processing Meson downloads pinned QAIC and Hexagon Open Access **19.0.02**, using system FastRPC or a source fallback. No full SDK is needed. Arch packaging enables CDSP; Debian disables it. See [CDSP build and runtime requirements](src/cdsp/README.md). Both CDSP and Vulkan support NV12/P010 cropping, scaling, conversion and clockwise 90°/180°/270° rotation, including combined operations. Dimensions and crop coordinates must be even, maximum dimension 8192, reduction up to 20:1, both axes scaling in the same direction after rotation. Color matrix/range is preserved; the base layer fills the output. No mirroring or interlacing. CDSP workers follow the available HVX count, up to eight. Select `IRIS_VAAPI_VPP=auto|cdsp|vulkan` at runtime or `-Dvpp_backend=...` at build time; the environment takes precedence. `auto` prefers CDSP for geometry/conversion and Vulkan for overlays, falling back to the available backend. `IRIS_VAAPI_OVERLAY` overrides only overlay selection. HDR tone mapping always requires Vulkan. ```sh ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 \ -hwaccel_output_format vaapi -i input.mp4 \ -vf scale_vaapi=w=3840:h=2160:format=nv12 -c:v hevc_vaapi -bf 0 output.mp4 ``` ## Vulkan video processing Use `tonemap_vaapi=format=nv12` for limited-range HDR10 P010 (BT.2020/PQ) to SDR NV12 (BT.709). Scaling/rotation can use either backend. Both backends support `overlay_vaapi`: BGRA/RGBA layers over NV12/P010, including crop, bilinear scaling, clipping, global and per-pixel alpha. Processing shares DMA-BUFs without CPU pixel copies. YUV overlays and luma keying are unsupported. `-Dvulkan_vpp=enabled` (default: `auto`) requires Vulkan ≥ 1.3 headers/loader and `glslangValidator`. Runtime needs a compatible Vulkan driver; tested with Mesa Turnip on Adreno 690. Both VPP backends use `/dev/dma_heap/system`, falling back to coherent MSM GEM on failure, never write-combined GEM. ## Usage Playback: ```sh mpv --hwdec=vaapi input.mp4 ``` H.264 encoding (for HEVC, replace the codec/profile with `hevc_vaapi`/`main`): ```sh ffmpeg -vaapi_device /dev/dri/renderD128 -i input.mp4 -an \ -vf format=nv12,hwupload -c:v h264_vaapi -profile:v high \ -bf 0 -g 60 -rc_mode CBR -b:v 6M -async_depth 4 output.mp4 ``` Hardware transcoding of supported 8-bit input: ```sh ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 \ -hwaccel_output_format vaapi -i input.mp4 -an \ -c:v h264_vaapi -bf 0 -qp 24 -async_depth 4 output.mp4 ``` - MP4 and raw H.264 output are supported; for Matroska, encode to raw H.264 first and remux. FFmpeg packed-header warnings are expected because firmware produces the headers. Custom VUI/SEI metadata is not forwarded. - GStreamer: set `GST_VA_ALL_DRIVERS=1` to enable this driver's `va` elements. - Sunshine/Moonlight: use **H.264 / SDR**; HEVC/AV1 probes may fail while H.264 remains available. ## Configuration and development | Variable | Purpose | | --- | --- | | `LIBVA_DRIVER_NAME=v4l2` | Explicitly select this backend. | | `LIBVA_DRIVERS_PATH=/path/to/build` | Load an uninstalled build. | | `IRIS_VAAPI_DEVICE` / `IRIS_VAAPI_ENCODER_DEVICE` | Override video-device discovery. | | `IRIS_VAAPI_COPY=gpu` or `cpu` | Default: GPU when built in, otherwise CPU. Explicit GPU requires GPU-copy support. | | `IRIS_VAAPI_DEBUG=1` | Enable diagnostics. | Use clang-format 22 with the checked-in `.clang-format` (Meson targets: `format`, `format-check`). Hardware test instructions are in [tests/](tests/). ## License [MIT](LICENSE) for the backend; GPL-2.0-only for bundled kernel patches. External test frameworks and media retain their own licenses. qcom-va-driver-1.1.0/debian/000077500000000000000000000000001526764642600156115ustar00rootroot00000000000000qcom-va-driver-1.1.0/debian/changelog000066400000000000000000000026021526764642600174630ustar00rootroot00000000000000qcom-va-driver (1.1.0-1) unstable; urgency=medium * Rename the project and package to qcom-va-driver, replacing libva-v4l2 without a transitional package and preserving driver names and session configuration paths. * Add AV1 Main 8/10-bit decoding on supported Iris devices. * Fix HEVC Main10 format handling, Chromium PRIME1 surface imports and decoder error recovery; bound CAPTURE buffer memory usage. * Replace FastCV with selectable CDSP/Vulkan video processing for scaling, cropping, rotation, NV12/P010 conversion and RGB overlay composition. * Add Vulkan HDR10-to-SDR tone mapping. Enable Vulkan video processing in Debian packages while keeping CDSP disabled. * Make GPU surface copying optional and improve VPP allocation fallback. * Generate pkg.conf for SC8280XP release and test repositories in CI. -- Xilin Wu Mon, 26 Oct 2026 20:50:01 +0800 libva-v4l2 (1.0.0-1) unstable; urgency=medium * Release 1.0.0. * Build one ARM64 package on Noble for Trixie, Noble, and Resolute. * Keep FastCV disabled and retain GStreamer VA session configuration. -- Xilin Wu Thu, 10 Sep 2026 17:03:16 +0800 libva-v4l2 (0.1.0-1) unstable; urgency=medium * Initial Debian packaging for RadxaOS. * Disable FastCV and retain GStreamer VA session configuration. -- Xilin Wu Thu, 10 Sep 2026 16:00:00 +0800 qcom-va-driver-1.1.0/debian/control000066400000000000000000000020161526764642600172130ustar00rootroot00000000000000Source: qcom-va-driver Section: video Priority: optional Maintainer: Xilin Wu Build-Depends: debhelper-compat (= 13), meson (>= 0.61), ninja-build, pkg-config, libva-dev, libdrm-dev, libegl-dev, libgles-dev, libgbm-dev, libvulkan-dev, glslang-tools Standards-Version: 4.7.0 Rules-Requires-Root: no Homepage: https://github.com/radxa-pkg/qcom-va-driver Vcs-Git: https://github.com/radxa-pkg/qcom-va-driver.git Vcs-Browser: https://github.com/radxa-pkg/qcom-va-driver Package: qcom-va-driver Architecture: arm64 Depends: ${shlibs:Depends}, ${misc:Depends} Recommends: mesa-vulkan-drivers Provides: va-driver Breaks: libva-v4l2 Replaces: libva-v4l2 Description: VA-API backend for Qualcomm Iris stateful V4L2 devices Hardware video decoding and H.264/HEVC encoding through the Qualcomm Iris stateful V4L2 M2M interface. This experimental driver targets SC8280XP, including Radxa Dragon Q8B, and requires a compatible kernel and firmware. . Includes the MSM driver alias and GStreamer VA environment configuration. qcom-va-driver-1.1.0/debian/copyright000066400000000000000000000027471526764642600175560ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: qcom-va-driver Source: https://github.com/radxa-pkg/qcom-va-driver Files: * Copyright: 2026 libva-v4l2 contributors License: MIT Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: . The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. . THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Files: patches/* Copyright: 2026 Xilin Wu Linux kernel contributors License: GPL-2.0-only On Debian systems the full text of the GNU General Public License version 2 can be found in /usr/share/common-licenses/GPL-2. qcom-va-driver-1.1.0/debian/install000066400000000000000000000001611526764642600172000ustar00rootroot00000000000000packaging/environment/60-libva-v4l2.conf usr/lib/environment.d packaging/environment/libva-v4l2.sh etc/profile.d qcom-va-driver-1.1.0/debian/rules000077500000000000000000000004571526764642600166770ustar00rootroot00000000000000#!/usr/bin/make -f export DEB_BUILD_MAINT_OPTIONS = hardening=+all %: dh $@ --buildsystem=meson override_dh_auto_configure: dh_auto_configure -- -Dgpu_copy=enabled -Dcdsp_vpp=disabled -Dvulkan_vpp=enabled override_dh_auto_install: dh_auto_install rm -rf debian/qcom-va-driver/usr/share/licenses qcom-va-driver-1.1.0/debian/source/000077500000000000000000000000001526764642600171115ustar00rootroot00000000000000qcom-va-driver-1.1.0/debian/source/format000066400000000000000000000000141526764642600203170ustar00rootroot000000000000003.0 (quilt) qcom-va-driver-1.1.0/meson.build000066400000000000000000000104711526764642600165340ustar00rootroot00000000000000project( 'qcom-va-driver', 'cpp', 'c', version: '1.1.0', meson_version: '>=0.61.0', license: 'MIT', default_options: ['cpp_std=c++17', 'warning_level=2'], ) va = dependency('libva', version: '>=1.10') drm = dependency('libdrm') threads = dependency('threads') gpu_copy_option = get_option('gpu_copy') egl = dependency('egl', required: gpu_copy_option) gles = dependency('glesv2', version: '>=3.2', required: gpu_copy_option) gbm = dependency('gbm', required: gpu_copy_option) have_gpu_copy = egl.found() and gles.found() and gbm.found() copy_dependencies = [] copy_source = files('src/cpu-copy.cpp') if have_gpu_copy copy_dependencies = [egl, gles, gbm] copy_source = files('src/gpu-copy.cpp') endif summary('GPU surface copy', have_gpu_copy) vpp_args = [] vpp_dependencies = [] cdsp_option = get_option('cdsp_vpp') have_cdsp_vpp = false if not cdsp_option.disabled() supported_build = build_machine.system() == 'linux' and build_machine.cpu_family() == 'aarch64' if cdsp_option.enabled() and not supported_build error('cdsp_vpp requires a Linux ARM64 build machine (Open Access 19.0.02)') endif if supported_build have_cdsp_vpp = find_program('ghc', native: true, required: cdsp_option).found() endif endif if have_cdsp_vpp subdir('src/cdsp') vpp_args = ['-DHAVE_CDSP_VPP', '-DIRIS_VPP_CLIENT_PATH="' + (get_option('prefix') / get_option('libdir') / 'qcom-va-driver/libiris_vpp.so') + '"'] vpp_dependencies = [meson.get_compiler('cpp').find_library('dl')] endif summary('CDSP VPP', have_cdsp_vpp) vulkan_option = get_option('vulkan_vpp') vulkan = dependency('vulkan', version: '>=1.3', required: vulkan_option) glslang = find_program('glslangValidator', native: true, required: vulkan_option) have_vulkan_vpp = not vulkan_option.disabled() and vulkan.found() and glslang.found() vulkan_sources = [] if have_vulkan_vpp subdir('src/vulkan') vpp_args += ['-DHAVE_VULKAN_VPP'] vpp_dependencies += [vulkan] endif summary('Vulkan VPP', have_vulkan_vpp) vpp_backend = get_option('vpp_backend') if (vpp_backend == 'cdsp' and not have_cdsp_vpp) or (vpp_backend == 'vulkan' and not have_vulkan_vpp) error('The selected vpp_backend must be enabled at build time') endif vpp_args += ['-DIRIS_VPP_DEFAULT_BACKEND="' + vpp_backend + '"'] sources = files( 'src/backend.cpp', 'src/av1.cpp', 'src/decoder.cpp', 'src/encoder.cpp', 'src/h264.cpp', 'src/hevc.cpp', 'src/msm.c', 'src/surface-copy.cpp', 'src/vp9.cpp', 'src/vpp.cpp', ) headers = files('src/internal.hpp') msm_allocation_test = executable('msm-allocation-test', 'tests/vpp/msm-allocation.c', 'src/msm.c', dependencies: drm, c_args: ['-UNDEBUG'], build_by_default: false) test('MSM allocation cache modes', msm_allocation_test) capture_pool_test = executable('capture-pool-test', 'tests/decoder/capture-pool.cpp', dependencies: [va, drm, threads], cpp_args: ['-UNDEBUG'], build_by_default: false) test('Decoder CAPTURE memory budget', capture_pool_test) driver_install_dir = get_option('libdir') / 'dri' shared_library( 'v4l2_drv_video', sources + copy_source + vulkan_sources, dependencies: [va, drm, threads] + copy_dependencies + vpp_dependencies, cpp_args: vpp_args, name_prefix: '', install: true, install_dir: driver_install_dir, ) # libva falls back to the DRM driver name on Qualcomm MSM devices. install_symlink( 'msm_drv_video.so', pointing_to: 'v4l2_drv_video.so', install_dir: driver_install_dir, install_tag: 'runtime', ) install_data('README.md', install_dir: get_option('datadir') / 'doc' / meson.project_name()) install_data('LICENSE', install_dir: get_option('datadir') / 'licenses' / meson.project_name()) formatter = find_program('clang-format', required: false) if formatter.found() sources += files('src/cpu-copy.cpp', 'src/gpu-copy.cpp', 'src/cdsp/client.cpp', 'src/cdsp/dsp.c', 'src/cdsp/filter.h', 'src/cdsp/client.h', 'src/overlay-params.h', 'src/cdsp/overlay.h', 'src/cdsp/qurt_abi.h', 'src/cdsp/qurt_abi_checks.h', 'src/cdsp/skel.cpp', 'src/cdsp/skel.h', 'src/cdsp/vpp_limits.h') sources += files('src/vulkan/vpp.cpp', 'src/vulkan/vpp.hpp') run_target('format', command: [formatter, '-i', '--style=file', sources, headers]) run_target('format-check', command: [formatter, '--dry-run', '--Werror', '--style=file', sources, headers]) endif qcom-va-driver-1.1.0/meson_options.txt000066400000000000000000000015561526764642600200330ustar00rootroot00000000000000option('gpu_copy', type: 'feature', value: 'auto', description: 'Surface copying with EGL, GLES 3.2, and GBM') option('cdsp_vpp', type: 'feature', value: 'enabled', description: 'Hardware-accelerated video processing on CDSP') option('vulkan_vpp', type: 'feature', value: 'auto', description: 'Hardware-accelerated video processing with Vulkan') option('vpp_backend', type: 'combo', choices: ['auto', 'cdsp', 'vulkan'], value: 'auto', description: 'Default backend for video processing') option('fastrpc', type: 'combo', choices: ['auto', 'system', 'bundled'], value: 'auto', description: 'FastRPC client for CDSP VPP') option('fastrpc_include_dir', type: 'string', value: '/usr/include/fastrpc', description: 'System FastRPC headers') option('cdsp_firmware_dir', type: 'string', value: 'lib/dsp/cdsp/qcom-va-driver', description: 'Install directory for the CDSP VPP skeleton') qcom-va-driver-1.1.0/packaging/000077500000000000000000000000001526764642600163135ustar00rootroot00000000000000qcom-va-driver-1.1.0/packaging/arch/000077500000000000000000000000001526764642600172305ustar00rootroot00000000000000qcom-va-driver-1.1.0/packaging/arch/PKGBUILD000066400000000000000000000043061526764642600203570ustar00rootroot00000000000000# SPDX-License-Identifier: MIT # Local source snapshot: python packaging/make-dist.py pkgname=qcom-va-driver pkgver=1.1.0 pkgrel=1 pkgdesc='VA-API backend for Qualcomm Iris stateful V4L2 M2M decoding and H.264/HEVC encoding' arch=('aarch64') url='https://github.com/radxa-pkg/qcom-va-driver' provides=("libva-v4l2=$pkgver") conflicts=('libva-v4l2') replaces=('libva-v4l2') license=('MIT' 'GPL-2.0-only') # backend MIT; bundled Linux patch GPL-2.0-only depends=('glibc' 'gcc-libs' 'libva' 'libdrm' 'mesa' 'libglvnd' 'qcom-fastrpc' 'vulkan-icd-loader' 'vulkan-freedreno') makedepends=('meson' 'ninja' 'ghc' 'haskell-syb' 'vulkan-headers' 'glslang') # Native strip cannot process the Hexagon DSP skeleton. options=('!strip') source=("$pkgname-$pkgver.tar.gz") # The archive is generated locally from the current working tree. sha256sums=('SKIP') prepare() { # Fetch required build tools here so build() can run with nodownload. # FastRPC is supplied by qcom-fastrpc, not the fallback subproject. meson subprojects download --sourcedir="$srcdir/$pkgname-$pkgver" qaic hexagon } build() { # make-dist.py fixes source mtimes to zero. Reusing Ninja objects from an # earlier snapshot can silently package the old driver despite new sources. rm -rf "$srcdir/build" arch-meson "$srcdir/$pkgname-$pkgver" "$srcdir/build" \ -Dgpu_copy=enabled -Dcdsp_vpp=enabled -Dfastrpc=system -Dvulkan_vpp=enabled meson compile -C "$srcdir/build" } package() { DESTDIR="$pkgdir" meson install -C "$srcdir/build" --no-rebuild cp -a "$srcdir/$pkgname-$pkgver/patches" "$pkgdir/usr/share/doc/$pkgname/" # Enable and prefer GStreamer VA elements in user sessions and login shells. local feature_rank='vah264dec:1000,vah264enc:1000,vah265dec:1000,vah265enc:1000,vavp9dec:1000' install -d "$pkgdir/usr/lib/environment.d" "$pkgdir/etc/profile.d" printf '%s\n' 'GST_VA_ALL_DRIVERS=1' \ "GST_PLUGIN_FEATURE_RANK=$feature_rank" \ > "$pkgdir/usr/lib/environment.d/60-libva-v4l2.conf" printf '%s\n' 'export GST_VA_ALL_DRIVERS=1' \ "export GST_PLUGIN_FEATURE_RANK=$feature_rank" \ > "$pkgdir/etc/profile.d/libva-v4l2.sh" chmod 644 "$pkgdir/usr/lib/environment.d/60-libva-v4l2.conf" \ "$pkgdir/etc/profile.d/libva-v4l2.sh" } qcom-va-driver-1.1.0/packaging/environment/000077500000000000000000000000001526764642600206575ustar00rootroot00000000000000qcom-va-driver-1.1.0/packaging/environment/60-libva-v4l2.conf000066400000000000000000000001671526764642600236370ustar00rootroot00000000000000GST_VA_ALL_DRIVERS=1 GST_PLUGIN_FEATURE_RANK=vah264dec:1000,vah264enc:1000,vah265dec:1000,vah265enc:1000,vavp9dec:1000 qcom-va-driver-1.1.0/packaging/environment/libva-v4l2.sh000066400000000000000000000002051526764642600230720ustar00rootroot00000000000000export GST_VA_ALL_DRIVERS=1 export GST_PLUGIN_FEATURE_RANK=vah264dec:1000,vah264enc:1000,vah265dec:1000,vah265enc:1000,vavp9dec:1000 qcom-va-driver-1.1.0/packaging/make-dist.py000066400000000000000000000033001526764642600205370ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT """Create a local userspace makepkg source archive from the current working tree.""" import argparse import gzip import io from pathlib import Path import re import tarfile def archive(destination, entries): # Fixed metadata makes archives independent of checkout timestamps/owners. with destination.open('wb') as raw, gzip.GzipFile(filename='', mode='wb', fileobj=raw, mtime=0) as gz: with tarfile.open(fileobj=gz, mode='w') as tar: for name, path in sorted(entries): data = path.read_bytes() info = tarfile.TarInfo(name) info.size = len(data) info.mode = 0o644 tar.addfile(info, io.BytesIO(data)) print(destination) def main(): ap = argparse.ArgumentParser(description=__doc__) ap.parse_args() root = Path(__file__).resolve().parent.parent version = re.search(r"version:\s*'([^']+)'", (root / 'meson.build').read_text())[1] name = 'qcom-va-driver-' + version paths = [root / p for p in ('meson.build', 'meson_options.txt', 'LICENSE', 'README.md', '.clang-format', '.editorconfig')] for directory in ('src', 'patches', 'tests'): paths += [p for p in (root / directory).rglob('*') if p.is_file() and '__pycache__' not in p.parts and p.suffix != '.pyc'] paths += list((root / 'subprojects').glob('*.wrap')) paths += [p for p in (root / 'subprojects/packagefiles').rglob('*') if p.is_file() and '__pycache__' not in p.parts] archive(root / 'packaging/arch' / (name + '.tar.gz'), [(name + '/' + str(p.relative_to(root)), p) for p in paths]) if __name__ == '__main__': main() qcom-va-driver-1.1.0/patches/000077500000000000000000000000001526764642600160165ustar00rootroot00000000000000qcom-va-driver-1.1.0/patches/0001-media-iris-support-decode-order-output-on-HFI-gen2.patch000066400000000000000000000162151526764642600307160ustar00rootroot00000000000000From e648c20ae7cb304a9f7ab86f95b5098a247a357c Mon Sep 17 00:00:00 2001 From: Xilin Wu Date: Tue, 8 Sep 2026 13:57:16 +0800 Subject: [PATCH 1/2] media: iris: support decode-order output on HFI gen2 Clients using synchronous per-picture decoding, such as VA-API users, can wait for a decoded picture before submitting further input. Firmware returning pictures in display order can withhold that picture until later input arrives, preventing such clients from making progress. Expose the standard decoder display-delay controls through the HFI gen2 decoder capability table. Support only a zero display delay, which maps to decode-order output, and leave the feature disabled by default. Use the existing control handling to reject changes while OUTPUT is streaming. Set HFI_PROP_DECODE_ORDER_OUTPUT through the decoder input configuration path before starting OUTPUT. Leave the firmware default untouched until userspace sets the enable control, and send its value on subsequent starts so disabling the control restores display-order output in the same session. Propagate failures through the configuration path. Signed-off-by: Xilin Wu --- drivers/media/platform/qcom/iris/iris_ctrls.c | 8 ++++++++ .../media/platform/qcom/iris/iris_hfi_gen2.c | 20 +++++++++++++++++++ .../qcom/iris/iris_hfi_gen2_command.c | 18 +++++++++++++++++ .../qcom/iris/iris_hfi_gen2_defines.h | 1 + .../platform/qcom/iris/iris_platform_common.h | 2 ++ 5 files changed, 49 insertions(+) diff --git a/drivers/media/platform/qcom/iris/iris_ctrls.c b/drivers/media/platform/qcom/iris/iris_ctrls.c index 29a0b6a09a55..1abf4d03e01e 100644 --- a/drivers/media/platform/qcom/iris/iris_ctrls.c +++ b/drivers/media/platform/qcom/iris/iris_ctrls.c @@ -154,6 +154,10 @@ static enum platform_inst_fw_cap_type iris_get_cap_id(u32 id) return LAYER4_BITRATE_HEVC; case V4L2_CID_MPEG_VIDEO_HEVC_HIER_CODING_L5_BR: return LAYER5_BITRATE_HEVC; + case V4L2_CID_MPEG_VIDEO_DEC_DISPLAY_DELAY_ENABLE: + return DISPLAY_DELAY_ENABLE; + case V4L2_CID_MPEG_VIDEO_DEC_DISPLAY_DELAY: + return DISPLAY_DELAY; case V4L2_CID_MPEG_VIDEO_FORCE_KEY_FRAME: return REQUEST_SYNC_FRAME; default: @@ -299,6 +303,10 @@ static u32 iris_get_v4l2_id(enum platform_inst_fw_cap_type cap_id) return V4L2_CID_MPEG_VIDEO_HEVC_HIER_CODING_L4_BR; case LAYER5_BITRATE_HEVC: return V4L2_CID_MPEG_VIDEO_HEVC_HIER_CODING_L5_BR; + case DISPLAY_DELAY_ENABLE: + return V4L2_CID_MPEG_VIDEO_DEC_DISPLAY_DELAY_ENABLE; + case DISPLAY_DELAY: + return V4L2_CID_MPEG_VIDEO_DEC_DISPLAY_DELAY; case REQUEST_SYNC_FRAME: return V4L2_CID_MPEG_VIDEO_FORCE_KEY_FRAME; default: diff --git a/drivers/media/platform/qcom/iris/iris_hfi_gen2.c b/drivers/media/platform/qcom/iris/iris_hfi_gen2.c index 3b8fbefb8b93..182b2a6b6de7 100644 --- a/drivers/media/platform/qcom/iris/iris_hfi_gen2.c +++ b/drivers/media/platform/qcom/iris/iris_hfi_gen2.c @@ -838,6 +838,22 @@ static const struct platform_inst_fw_cap inst_fw_cap_milos_enc[] = { }; static const struct platform_inst_fw_cap inst_fw_cap_sm8550_dec[] = { + { + .cap_id = DISPLAY_DELAY_ENABLE, + .min = 0, + .max = 1, + .step_or_mask = 1, + .value = 0, + .hfi_id = HFI_PROP_DECODE_ORDER_OUTPUT, + .flags = CAP_FLAG_INPUT_PORT, + }, + { + .cap_id = DISPLAY_DELAY, + .min = 0, + .max = 0, + .step_or_mask = 1, + .value = 0, + }, { .cap_id = PROFILE_H264, .min = V4L2_MPEG_VIDEO_H264_PROFILE_BASELINE, @@ -1785,6 +1801,7 @@ static const struct platform_inst_fw_cap inst_fw_cap_sm8550_enc[] = { }; static const u32 sm8550_vdec_input_config_params_default[] = { + HFI_PROP_DECODE_ORDER_OUTPUT, HFI_PROP_BITSTREAM_RESOLUTION, HFI_PROP_CROP_OFFSETS, HFI_PROP_LUMA_CHROMA_BIT_DEPTH, @@ -1797,6 +1814,7 @@ static const u32 sm8550_vdec_input_config_params_default[] = { }; static const u32 sm8550_vdec_input_config_param_hevc[] = { + HFI_PROP_DECODE_ORDER_OUTPUT, HFI_PROP_BITSTREAM_RESOLUTION, HFI_PROP_CROP_OFFSETS, HFI_PROP_LUMA_CHROMA_BIT_DEPTH, @@ -1808,6 +1826,7 @@ static const u32 sm8550_vdec_input_config_param_hevc[] = { }; static const u32 sm8550_vdec_input_config_param_vp9[] = { + HFI_PROP_DECODE_ORDER_OUTPUT, HFI_PROP_BITSTREAM_RESOLUTION, HFI_PROP_CROP_OFFSETS, HFI_PROP_LUMA_CHROMA_BIT_DEPTH, @@ -1817,6 +1836,7 @@ static const u32 sm8550_vdec_input_config_param_vp9[] = { }; static const u32 sm8550_vdec_input_config_param_av1[] = { + HFI_PROP_DECODE_ORDER_OUTPUT, HFI_PROP_BITSTREAM_RESOLUTION, HFI_PROP_CROP_OFFSETS, HFI_PROP_LUMA_CHROMA_BIT_DEPTH, diff --git a/drivers/media/platform/qcom/iris/iris_hfi_gen2_command.c b/drivers/media/platform/qcom/iris/iris_hfi_gen2_command.c index 7038f83c3aeb..1d596b1c7b1b 100644 --- a/drivers/media/platform/qcom/iris/iris_hfi_gen2_command.c +++ b/drivers/media/platform/qcom/iris/iris_hfi_gen2_command.c @@ -667,6 +667,23 @@ static int iris_hfi_gen2_set_super_block(struct iris_inst *inst, u32 plane) sizeof(u32)); } +static int iris_hfi_gen2_set_decode_order_output(struct iris_inst *inst, u32 plane) +{ + struct platform_inst_fw_cap *cap = &inst->fw_caps[DISPLAY_DELAY_ENABLE]; + u32 decode_order = cap->value; + + /* Leave the firmware default unchanged unless userspace sets the control. */ + if (!(cap->flags & CAP_FLAG_CLIENT_SET)) + return 0; + + return iris_hfi_gen2_session_set_property(inst, + HFI_PROP_DECODE_ORDER_OUTPUT, + HFI_HOST_FLAGS_NONE, + HFI_PORT_BITSTREAM, + HFI_PAYLOAD_U32, + &decode_order, sizeof(decode_order)); +} + static int iris_hfi_gen2_session_set_config_params(struct iris_inst *inst, u32 plane) { const struct iris_firmware_data *fdata = inst->core->iris_firmware_data; @@ -693,6 +710,7 @@ static int iris_hfi_gen2_session_set_config_params(struct iris_inst *inst, u32 p {HFI_PROP_FRAME_RATE, iris_hfi_gen2_set_frame_rate }, {HFI_PROP_AV1_FILM_GRAIN_PRESENT, iris_hfi_gen2_set_film_grain }, {HFI_PROP_AV1_SUPER_BLOCK_ENABLED, iris_hfi_gen2_set_super_block }, + {HFI_PROP_DECODE_ORDER_OUTPUT, iris_hfi_gen2_set_decode_order_output }, }; if (inst->domain == DECODER) { diff --git a/drivers/media/platform/qcom/iris/iris_hfi_gen2_defines.h b/drivers/media/platform/qcom/iris/iris_hfi_gen2_defines.h index f43aea10090d..5a56536a7c2c 100644 --- a/drivers/media/platform/qcom/iris/iris_hfi_gen2_defines.h +++ b/drivers/media/platform/qcom/iris/iris_hfi_gen2_defines.h @@ -128,6 +128,7 @@ enum hfi_flip { #define HFI_PROP_FLIP 0x0300014c #define HFI_PROP_SIGNAL_COLOR_INFO 0x03000155 +#define HFI_PROP_DECODE_ORDER_OUTPUT 0x0300015b #define HFI_PROP_PICTURE_TYPE 0x03000162 #define HFI_PROP_DEC_DEFAULT_HEADER 0x03000168 #define HFI_PROP_DEC_START_FROM_RAP_FRAME 0x03000169 diff --git a/drivers/media/platform/qcom/iris/iris_platform_common.h b/drivers/media/platform/qcom/iris/iris_platform_common.h index a193c8a1b445..a4422b09050d 100644 --- a/drivers/media/platform/qcom/iris/iris_platform_common.h +++ b/drivers/media/platform/qcom/iris/iris_platform_common.h @@ -187,6 +187,8 @@ enum platform_inst_fw_cap_type { LAYER5_BITRATE_HEVC, REQUEST_SYNC_FRAME, TIME_DELTA_BASED_RC, + DISPLAY_DELAY_ENABLE, + DISPLAY_DELAY, INST_FW_CAP_MAX, }; -- 2.55.0 qcom-va-driver-1.1.0/patches/0002-media-iris-allow-larger-capture-buffer-pools-on-HFI-.patch000066400000000000000000000030761526764642600311720ustar00rootroot00000000000000From abe302bc460e5014baa43bcf181731c3766392be Mon Sep 17 00:00:00 2001 From: Xilin Wu Date: Tue, 8 Sep 2026 13:57:16 +0800 Subject: [PATCH 2/2] media: iris: allow larger capture buffer pools on HFI gen2 Threaded video clients can retain more than 32 decoded surfaces before returning them to the decoder. For example, VLC using VA-API can request more surfaces with its default frame threading. The default vb2 limit of 32 buffers prevents these clients from allocating the capture pool they need. Set the HFI gen2 decoder CAPTURE queue limit to DEFAULT_MAX_HOST_BUF_COUNT, allowing up to 64 buffers. This only raises the maximum userspace can request; the minimum allocation, OUTPUT queue and encoder queues remain unchanged. Signed-off-by: Xilin Wu --- drivers/media/platform/qcom/iris/iris_vidc.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/media/platform/qcom/iris/iris_vidc.c b/drivers/media/platform/qcom/iris/iris_vidc.c index 2afb03a13a50..5ff14172c12a 100644 --- a/drivers/media/platform/qcom/iris/iris_vidc.c +++ b/drivers/media/platform/qcom/iris/iris_vidc.c @@ -122,6 +122,9 @@ iris_m2m_queue_init(void *priv, struct vb2_queue *src_vq, struct vb2_queue *dst_ dst_vq->drv_priv = inst; dst_vq->buf_struct_size = sizeof(struct iris_buffer); dst_vq->min_reqbufs_allocation = MIN_BUFFERS; + if (inst->domain == DECODER && + inst->core->iris_firmware_data == &iris_hfi_gen2_data) + dst_vq->max_num_buffers = DEFAULT_MAX_HOST_BUF_COUNT; dst_vq->dev = inst->core->dev; dst_vq->lock = &inst->ctx_q_lock; -- 2.55.0 qcom-va-driver-1.1.0/patches/0003-media-iris-Add-deblocking-filter-controls-for-HFI-ge.patch000066400000000000000000000265111526764642600311410ustar00rootroot00000000000000From c616201608bb7b20a021a551e4b92f5a1b5464f8 Mon Sep 17 00:00:00 2001 From: Xilin Wu Date: Tue, 8 Sep 2026 22:40:49 +0800 Subject: [PATCH] media: iris: Add deblocking filter controls for HFI gen2 Iris does not expose deblocking filter controls for HFI gen2 encoder sessions, leaving applications unable to configure the filter mode or its offsets. Expose the standard V4L2 H.264 loop filter mode, alpha and beta controls and the HEVC loop filter mode, tc and beta controls. Register them in both gen2 encoder capability tables with filtering enabled and zero offsets by default. Allow changes before streaming starts. Translate the codec-specific mode enums and pack the offsets, biased by six, into HFI_PROP_DEBLOCKING_MODE. Only send the controls for the active codec so that the other codec's settings cannot overwrite them. Signed-off-by: Xilin Wu --- drivers/media/platform/qcom/iris/iris_ctrls.c | 85 +++++++++++++ drivers/media/platform/qcom/iris/iris_ctrls.h | 1 + .../media/platform/qcom/iris/iris_hfi_gen2.c | 116 ++++++++++++++++++ .../qcom/iris/iris_hfi_gen2_defines.h | 8 ++ .../platform/qcom/iris/iris_platform_common.h | 6 + 5 files changed, 216 insertions(+) diff --git a/drivers/media/platform/qcom/iris/iris_ctrls.c b/drivers/media/platform/qcom/iris/iris_ctrls.c index 9b148f4a1c38..dcc8ebe49206 100644 --- a/drivers/media/platform/qcom/iris/iris_ctrls.c +++ b/drivers/media/platform/qcom/iris/iris_ctrls.c @@ -54,6 +54,18 @@ static enum platform_inst_fw_cap_type iris_get_cap_id(u32 id) return GOP_SIZE; case V4L2_CID_MPEG_VIDEO_H264_ENTROPY_MODE: return ENTROPY_MODE; + case V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_MODE: + return DEBLOCK_MODE_H264; + case V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_ALPHA: + return DEBLOCK_ALPHA_H264; + case V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_BETA: + return DEBLOCK_BETA_H264; + case V4L2_CID_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE: + return DEBLOCK_MODE_HEVC; + case V4L2_CID_MPEG_VIDEO_HEVC_LF_TC_OFFSET_DIV2: + return DEBLOCK_TC_HEVC; + case V4L2_CID_MPEG_VIDEO_HEVC_LF_BETA_OFFSET_DIV2: + return DEBLOCK_BETA_HEVC; case V4L2_CID_MPEG_VIDEO_H264_MIN_QP: return MIN_FRAME_QP_H264; case V4L2_CID_MPEG_VIDEO_HEVC_MIN_QP: @@ -209,6 +221,18 @@ static u32 iris_get_v4l2_id(enum platform_inst_fw_cap_type cap_id) return V4L2_CID_MPEG_VIDEO_GOP_SIZE; case ENTROPY_MODE: return V4L2_CID_MPEG_VIDEO_H264_ENTROPY_MODE; + case DEBLOCK_MODE_H264: + return V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_MODE; + case DEBLOCK_ALPHA_H264: + return V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_ALPHA; + case DEBLOCK_BETA_H264: + return V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_BETA; + case DEBLOCK_MODE_HEVC: + return V4L2_CID_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE; + case DEBLOCK_TC_HEVC: + return V4L2_CID_MPEG_VIDEO_HEVC_LF_TC_OFFSET_DIV2; + case DEBLOCK_BETA_HEVC: + return V4L2_CID_MPEG_VIDEO_HEVC_LF_BETA_OFFSET_DIV2; case MIN_FRAME_QP_H264: return V4L2_CID_MPEG_VIDEO_H264_MIN_QP; case MIN_FRAME_QP_HEVC: @@ -885,6 +909,67 @@ int iris_set_entropy_mode_gen2(struct iris_inst *inst, enum platform_inst_fw_cap &entropy_mode, sizeof(u32)); } +int iris_set_deblock_gen2(struct iris_inst *inst, enum platform_inst_fw_cap_type cap_id) +{ + const struct iris_hfi_session_ops *hfi_ops = inst->hfi_session_ops; + u32 hfi_id = inst->fw_caps[cap_id].hfi_id; + u32 mode = inst->fw_caps[cap_id].value; + u32 alpha_tc, beta, hfi_value; + + if (inst->codec == V4L2_PIX_FMT_H264) { + if (cap_id != DEBLOCK_MODE_H264) + return 0; + + switch (mode) { + case V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_ENABLED: + mode = HFI_DEBLOCK_ALL_BOUNDARY; + break; + case V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_DISABLED: + mode = HFI_DEBLOCK_DISABLE; + break; + case V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_DISABLED_AT_SLICE_BOUNDARY: + mode = HFI_DEBLOCK_DISABLE_AT_SLICE_BOUNDARY; + break; + default: + return -EINVAL; + } + + alpha_tc = inst->fw_caps[DEBLOCK_ALPHA_H264].value + 6; + beta = inst->fw_caps[DEBLOCK_BETA_H264].value + 6; + } else if (inst->codec == V4L2_PIX_FMT_HEVC) { + if (cap_id != DEBLOCK_MODE_HEVC) + return 0; + + switch (mode) { + case V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_ENABLED: + mode = HFI_DEBLOCK_ALL_BOUNDARY; + break; + case V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_DISABLED: + mode = HFI_DEBLOCK_DISABLE; + break; + case V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_DISABLED_AT_SLICE_BOUNDARY: + mode = HFI_DEBLOCK_DISABLE_AT_SLICE_BOUNDARY; + break; + default: + return -EINVAL; + } + + alpha_tc = inst->fw_caps[DEBLOCK_TC_HEVC].value + 6; + beta = inst->fw_caps[DEBLOCK_BETA_HEVC].value + 6; + } else { + return 0; + } + + /* Firmware expects offsets biased by 6, with alpha or HEVC tc in bits 23:16. */ + hfi_value = alpha_tc << 16 | beta << 8 | mode; + + return hfi_ops->session_set_property(inst, hfi_id, + HFI_HOST_FLAGS_NONE, + iris_get_port_info(inst, cap_id), + HFI_PAYLOAD_32_PACKED, + &hfi_value, sizeof(hfi_value)); +} + int iris_set_min_qp(struct iris_inst *inst, enum platform_inst_fw_cap_type cap_id) { const struct iris_hfi_session_ops *hfi_ops = inst->hfi_session_ops; diff --git a/drivers/media/platform/qcom/iris/iris_ctrls.h b/drivers/media/platform/qcom/iris/iris_ctrls.h index 08db80744420..82437f4e19c1 100644 --- a/drivers/media/platform/qcom/iris/iris_ctrls.h +++ b/drivers/media/platform/qcom/iris/iris_ctrls.h @@ -29,6 +29,7 @@ int iris_set_bitrate_mode_gen1(struct iris_inst *inst, enum platform_inst_fw_cap int iris_set_bitrate_mode_gen2(struct iris_inst *inst, enum platform_inst_fw_cap_type cap_id); int iris_set_entropy_mode_gen1(struct iris_inst *inst, enum platform_inst_fw_cap_type cap_id); int iris_set_entropy_mode_gen2(struct iris_inst *inst, enum platform_inst_fw_cap_type cap_id); +int iris_set_deblock_gen2(struct iris_inst *inst, enum platform_inst_fw_cap_type cap_id); int iris_set_min_qp(struct iris_inst *inst, enum platform_inst_fw_cap_type cap_id); int iris_set_max_qp(struct iris_inst *inst, enum platform_inst_fw_cap_type cap_id); int iris_set_frame_qp(struct iris_inst *inst, enum platform_inst_fw_cap_type cap_id); diff --git a/drivers/media/platform/qcom/iris/iris_hfi_gen2.c b/drivers/media/platform/qcom/iris/iris_hfi_gen2.c index 63ca0e8e98b0..917e24a0a317 100644 --- a/drivers/media/platform/qcom/iris/iris_hfi_gen2.c +++ b/drivers/media/platform/qcom/iris/iris_hfi_gen2.c @@ -355,6 +355,64 @@ static const struct platform_inst_fw_cap inst_fw_cap_milos_enc[] = { .flags = CAP_FLAG_OUTPUT_PORT | CAP_FLAG_MENU, .set = iris_set_entropy_mode_gen2, }, + { + .cap_id = DEBLOCK_MODE_H264, + .min = V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_ENABLED, + .max = V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_DISABLED_AT_SLICE_BOUNDARY, + .step_or_mask = + BIT(V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_ENABLED) | + BIT(V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_DISABLED) | + BIT(V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_DISABLED_AT_SLICE_BOUNDARY), + .value = V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_ENABLED, + .hfi_id = HFI_PROP_DEBLOCKING_MODE, + .flags = CAP_FLAG_OUTPUT_PORT | CAP_FLAG_MENU, + .set = iris_set_deblock_gen2, + }, + { + .cap_id = DEBLOCK_ALPHA_H264, + .min = -6, + .max = 6, + .step_or_mask = 1, + .value = 0, + .flags = CAP_FLAG_OUTPUT_PORT, + }, + { + .cap_id = DEBLOCK_BETA_H264, + .min = -6, + .max = 6, + .step_or_mask = 1, + .value = 0, + .flags = CAP_FLAG_OUTPUT_PORT, + }, + { + .cap_id = DEBLOCK_MODE_HEVC, + .min = V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_DISABLED, + .max = V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_DISABLED_AT_SLICE_BOUNDARY, + .step_or_mask = + BIT(V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_ENABLED) | + BIT(V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_DISABLED) | + BIT(V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_DISABLED_AT_SLICE_BOUNDARY), + .value = V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_ENABLED, + .hfi_id = HFI_PROP_DEBLOCKING_MODE, + .flags = CAP_FLAG_OUTPUT_PORT | CAP_FLAG_MENU, + .set = iris_set_deblock_gen2, + }, + { + .cap_id = DEBLOCK_TC_HEVC, + .min = -6, + .max = 6, + .step_or_mask = 1, + .value = 0, + .flags = CAP_FLAG_OUTPUT_PORT, + }, + { + .cap_id = DEBLOCK_BETA_HEVC, + .min = -6, + .max = 6, + .step_or_mask = 1, + .value = 0, + .flags = CAP_FLAG_OUTPUT_PORT, + }, { .cap_id = MIN_FRAME_QP_H264, .min = MIN_QP_8BIT, @@ -1307,6 +1365,64 @@ static const struct platform_inst_fw_cap inst_fw_cap_sm8550_enc[] = { .flags = CAP_FLAG_OUTPUT_PORT | CAP_FLAG_MENU, .set = iris_set_entropy_mode_gen2, }, + { + .cap_id = DEBLOCK_MODE_H264, + .min = V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_ENABLED, + .max = V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_DISABLED_AT_SLICE_BOUNDARY, + .step_or_mask = + BIT(V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_ENABLED) | + BIT(V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_DISABLED) | + BIT(V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_DISABLED_AT_SLICE_BOUNDARY), + .value = V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_ENABLED, + .hfi_id = HFI_PROP_DEBLOCKING_MODE, + .flags = CAP_FLAG_OUTPUT_PORT | CAP_FLAG_MENU, + .set = iris_set_deblock_gen2, + }, + { + .cap_id = DEBLOCK_ALPHA_H264, + .min = -6, + .max = 6, + .step_or_mask = 1, + .value = 0, + .flags = CAP_FLAG_OUTPUT_PORT, + }, + { + .cap_id = DEBLOCK_BETA_H264, + .min = -6, + .max = 6, + .step_or_mask = 1, + .value = 0, + .flags = CAP_FLAG_OUTPUT_PORT, + }, + { + .cap_id = DEBLOCK_MODE_HEVC, + .min = V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_DISABLED, + .max = V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_DISABLED_AT_SLICE_BOUNDARY, + .step_or_mask = + BIT(V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_ENABLED) | + BIT(V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_DISABLED) | + BIT(V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_DISABLED_AT_SLICE_BOUNDARY), + .value = V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_ENABLED, + .hfi_id = HFI_PROP_DEBLOCKING_MODE, + .flags = CAP_FLAG_OUTPUT_PORT | CAP_FLAG_MENU, + .set = iris_set_deblock_gen2, + }, + { + .cap_id = DEBLOCK_TC_HEVC, + .min = -6, + .max = 6, + .step_or_mask = 1, + .value = 0, + .flags = CAP_FLAG_OUTPUT_PORT, + }, + { + .cap_id = DEBLOCK_BETA_HEVC, + .min = -6, + .max = 6, + .step_or_mask = 1, + .value = 0, + .flags = CAP_FLAG_OUTPUT_PORT, + }, { .cap_id = MIN_FRAME_QP_H264, .min = MIN_QP_8BIT, diff --git a/drivers/media/platform/qcom/iris/iris_hfi_gen2_defines.h b/drivers/media/platform/qcom/iris/iris_hfi_gen2_defines.h index e89ed4075526..04e5429328eb 100644 --- a/drivers/media/platform/qcom/iris/iris_hfi_gen2_defines.h +++ b/drivers/media/platform/qcom/iris/iris_hfi_gen2_defines.h @@ -57,6 +57,14 @@ #define HFI_PROP_BUFFER_FW_MIN_OUTPUT_COUNT 0x03000124 #define HFI_PROP_PIC_ORDER_CNT_TYPE 0x03000128 +enum hfi_deblock_mode { + HFI_DEBLOCK_ALL_BOUNDARY = 0x0, + HFI_DEBLOCK_DISABLE = 0x1, + HFI_DEBLOCK_DISABLE_AT_SLICE_BOUNDARY = 0x2, +}; + +#define HFI_PROP_DEBLOCKING_MODE 0x03000129 + enum hfi_rate_control { HFI_RC_VBR_CFR = 0x00000000, HFI_RC_CBR_CFR = 0x00000001, diff --git a/drivers/media/platform/qcom/iris/iris_platform_common.h b/drivers/media/platform/qcom/iris/iris_platform_common.h index d265d370cc74..b109ab22e8bd 100644 --- a/drivers/media/platform/qcom/iris/iris_platform_common.h +++ b/drivers/media/platform/qcom/iris/iris_platform_common.h @@ -150,6 +150,12 @@ enum platform_inst_fw_cap_type { FRAME_RC_ENABLE, GOP_SIZE, ENTROPY_MODE, + DEBLOCK_MODE_H264, + DEBLOCK_ALPHA_H264, + DEBLOCK_BETA_H264, + DEBLOCK_MODE_HEVC, + DEBLOCK_TC_HEVC, + DEBLOCK_BETA_HEVC, MIN_FRAME_QP_H264, MIN_FRAME_QP_HEVC, MAX_FRAME_QP_H264, -- 2.55.0 qcom-va-driver-1.1.0/patches/0004-media-iris-Preserve-timestamps-on-error-buffers.patch000066400000000000000000000033661526764642600306230ustar00rootroot00000000000000From 02a49632a858612e321a9136e3e9ef87c1af2257 Mon Sep 17 00:00:00 2001 From: Xilin Wu Date: Wed, 21 Oct 2026 04:07:34 +0000 Subject: [PATCH] media: iris: Preserve timestamps on error buffers Iris clears the timestamp when returning a buffer with V4L2_BUF_FLAG_ERROR, even if firmware supplied a valid timestamp. With a corrupted HEVC frame on HFI gen2, firmware returns the frame with HFI_INFO_DATA_CORRUPT and its original timestamp, but userspace receives an empty error buffer with timestamp zero. This prevents clients such as libva-v4l2 from identifying the failed picture and can leave them waiting for it until a synchronization timeout. Copy the recorded timestamp to error buffers so userspace can associate the error with the corresponding input. Keep the error state and zero payload unchanged; this does not assume that the corrupted image is suitable for display. Fixes: b791dcfcba3a ("media: iris: Add handling for corrupt and drop frames") Signed-off-by: Xilin Wu --- drivers/media/platform/qcom/iris/iris_buffer.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/media/platform/qcom/iris/iris_buffer.c b/drivers/media/platform/qcom/iris/iris_buffer.c index fdcf46076f92..d0d9641ea335 100644 --- a/drivers/media/platform/qcom/iris/iris_buffer.c +++ b/drivers/media/platform/qcom/iris/iris_buffer.c @@ -965,7 +965,8 @@ int iris_vb2_buffer_done(struct iris_inst *inst, struct iris_buffer *buf) if (buf->flags & V4L2_BUF_FLAG_ERROR) { state = VB2_BUF_STATE_ERROR; vb2_set_plane_payload(vb2, 0, 0); - vb2->timestamp = 0; + /* Allow userspace to associate the error with the input frame. */ + vb2->timestamp = buf->timestamp; v4l2_m2m_buf_done(vbuf, state); return 0; } -- 2.56.0 qcom-va-driver-1.1.0/patches/0005-media-iris-Recycle-decoder-buffers-for-non-display-p.patch000066400000000000000000000146731526764642600313540ustar00rootroot00000000000000From 841f40bbd2c8aedb2d7fb003ba67ce78b5d04209 Mon Sep 17 00:00:00 2001 From: Xilin Wu Date: Mon, 26 Oct 2026 12:08:54 +0800 Subject: [PATCH] media: iris: Recycle decoder buffers for non-display pictures HFI gen2 marks non-display pictures with HFI_GEN2_PICTURE_NOSHOW. These are normal decoding results, but Iris maps them to V4L2_BUF_FLAG_ERROR and returns an empty CAPTURE buffer to userspace. For AV1, a hidden reference picture and a displayed picture can share the same input temporal unit and timestamp. GStreamer's v4l2av1dec drops the pending frame on the error return, losing the subsequent displayed picture. Recycle successful NOSHOW CAPTURE buffers within the driver instead of completing them through vb2. Preserve genuine corruption and overflow errors, and return empty LAST buffers for non-display pictures at EOS or a resolution change. Exclude both kinds of LAST buffer from the empty-payload error check. Do not submit recycled buffers while a CAPTURE STOP response is pending, since the response wait releases the instance lock. Leave those buffers deferred for streamoff cleanup. If requeuing fails, return the buffer with an error and its original timestamp. On X1E80100, 60-frame AV1 lookahead samples now produce all 60 frames instead of 48 with GStreamer, for both 8-bit and 10-bit output. Direct V4L2 and GStreamer decoding of seven samples from 720p to 4K matches dav1d byte for byte. Fixes: ac72ad9deafa ("media: iris: Add handling for no show frames") Signed-off-by: Xilin Wu --- .../media/platform/qcom/iris/iris_hfi_gen2.h | 2 + .../qcom/iris/iris_hfi_gen2_command.c | 11 ++++- .../qcom/iris/iris_hfi_gen2_response.c | 47 +++++++++++++++++-- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/drivers/media/platform/qcom/iris/iris_hfi_gen2.h b/drivers/media/platform/qcom/iris/iris_hfi_gen2.h index 21ab58e0aa84..79aac2d4e500 100644 --- a/drivers/media/platform/qcom/iris/iris_hfi_gen2.h +++ b/drivers/media/platform/qcom/iris/iris_hfi_gen2.h @@ -20,6 +20,7 @@ struct iris_core; * @packet: HFI packet * @ipsc_properties_set: boolean to set ipsc properties to fw * @opsc_properties_set: boolean to set opsc properties to fw + * @output_stopping: capture STOP command is awaiting its response * @hfi_frame_info: structure of frame info * @src_subcr_params: subscription params to fw on input port * @dst_subcr_params: subscription params to fw on output port @@ -29,6 +30,7 @@ struct iris_inst_hfi_gen2 { struct iris_hfi_header *packet; bool ipsc_properties_set; bool opsc_properties_set; + bool output_stopping; struct iris_hfi_frame_info hfi_frame_info; struct hfi_subscription_params src_subcr_params; struct hfi_subscription_params dst_subcr_params; diff --git a/drivers/media/platform/qcom/iris/iris_hfi_gen2_command.c b/drivers/media/platform/qcom/iris/iris_hfi_gen2_command.c index 1d596b1c7b1b..337367bcc7a4 100644 --- a/drivers/media/platform/qcom/iris/iris_hfi_gen2_command.c +++ b/drivers/media/platform/qcom/iris/iris_hfi_gen2_command.c @@ -1174,7 +1174,16 @@ static int iris_hfi_gen2_session_stop(struct iris_inst *inst, u32 plane) if (ret) return ret; - return iris_wait_for_session_response(inst, false); + /* The response wait drops inst->lock, allowing buffers to be returned. */ + if (plane == V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE) + inst_hfi_gen2->output_stopping = true; + + ret = iris_wait_for_session_response(inst, false); + + if (plane == V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE) + inst_hfi_gen2->output_stopping = false; + + return ret; } static int iris_hfi_gen2_session_pause(struct iris_inst *inst, u32 plane) diff --git a/drivers/media/platform/qcom/iris/iris_hfi_gen2_response.c b/drivers/media/platform/qcom/iris/iris_hfi_gen2_response.c index d9ac8dca61ae..1f47a6d65c1a 100644 --- a/drivers/media/platform/qcom/iris/iris_hfi_gen2_response.c +++ b/drivers/media/platform/qcom/iris/iris_hfi_gen2_response.c @@ -101,9 +101,7 @@ static int iris_hfi_gen2_get_driver_buffer_flags(struct iris_inst *inst, u32 hfi HFI_GEN2_PICTURE_CRA | HFI_GEN2_PICTURE_BLA; u32 driver_flags = 0; - if (inst_hfi_gen2->hfi_frame_info.picture_type & HFI_GEN2_PICTURE_NOSHOW) - driver_flags |= V4L2_BUF_FLAG_ERROR; - else if (inst_hfi_gen2->hfi_frame_info.picture_type & keyframe) + if (inst_hfi_gen2->hfi_frame_info.picture_type & keyframe) driver_flags |= V4L2_BUF_FLAG_KEYFRAME; else if (inst_hfi_gen2->hfi_frame_info.picture_type & HFI_GEN2_PICTURE_P) driver_flags |= V4L2_BUF_FLAG_PFRAME; @@ -339,9 +337,40 @@ static int iris_hfi_gen2_handle_input_buffer(struct iris_inst *inst, return 0; } +static int iris_hfi_gen2_requeue_output_buffer(struct iris_inst *inst, + struct iris_buffer *buf) +{ + struct iris_inst_hfi_gen2 *inst_hfi_gen2 = to_iris_inst_hfi_gen2(inst); + u64 timestamp = buf->timestamp; + int ret; + + buf->data_size = 0; + buf->timestamp = 0; + buf->flags = 0; + buf->attr &= ~BUF_ATTR_DEQUEUED; + buf->attr |= BUF_ATTR_DEFERRED; + + /* Leave stopped buffers for streamoff cleanup or the next streamon. */ + if (inst_hfi_gen2->output_stopping || + (inst->state != IRIS_INST_STREAMING && + inst->state != IRIS_INST_OUTPUT_STREAMING)) + return 0; + + ret = iris_queue_buffer(inst, buf); + if (ret) { + buf->attr &= ~BUF_ATTR_DEFERRED; + buf->attr |= BUF_ATTR_DEQUEUED; + buf->flags = V4L2_BUF_FLAG_ERROR; + buf->timestamp = timestamp; + } + + return ret; +} + static int iris_hfi_gen2_handle_output_buffer(struct iris_inst *inst, struct iris_hfi_buffer *hfi_buffer) { + struct iris_inst_hfi_gen2 *inst_hfi_gen2 = to_iris_inst_hfi_gen2(inst); struct v4l2_m2m_ctx *m2m_ctx = inst->m2m_ctx; struct v4l2_m2m_buffer *m2m_buffer, *n; struct iris_buffer *buf; @@ -384,8 +413,18 @@ static int iris_hfi_gen2_handle_output_buffer(struct iris_inst *inst, buf->flags = iris_hfi_gen2_get_driver_buffer_flags(inst, hfi_buffer->flags); + if (inst->domain == DECODER && + inst_hfi_gen2->hfi_frame_info.picture_type & HFI_GEN2_PICTURE_NOSHOW) { + /* A hidden reference picture is neither a display frame nor an error. */ + buf->data_size = 0; + if (inst->state == IRIS_INST_ERROR) + buf->flags |= V4L2_BUF_FLAG_ERROR; + if (!(buf->flags & (V4L2_BUF_FLAG_ERROR | V4L2_BUF_FLAG_LAST))) + return iris_hfi_gen2_requeue_output_buffer(inst, buf); + } + if (!buf->data_size && inst->state == IRIS_INST_STREAMING && - !(hfi_buffer->flags & HFI_BUF_FW_FLAG_LAST)) { + !(buf->flags & V4L2_BUF_FLAG_LAST)) { buf->flags |= V4L2_BUF_FLAG_ERROR; } qcom-va-driver-1.1.0/src/000077500000000000000000000000001526764642600151565ustar00rootroot00000000000000qcom-va-driver-1.1.0/src/av1.cpp000066400000000000000000000670151526764642600163620ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "internal.hpp" namespace irisva { namespace { // VA provides parsed picture parameters and raw tiles, but Iris expects OBUs. // The reference map below bridges VA surfaces and the firmware's eight slots. constexpr VAStatus invalid = VA_STATUS_ERROR_INVALID_BUFFER; constexpr unsigned key_frame = 0, inter_frame = 1, intra_only = 2, switch_frame = 3; unsigned log2_ceil(unsigned n) { unsigned bits = 0; for (unsigned v = n - 1; v; v >>= 1) ++bits; return bits; } unsigned tile_log2(unsigned block, unsigned target) { unsigned bits = 0; while ((block << bits) < target) ++bits; return bits; } class Bits { std::vector bytes_; unsigned position_ = 0; public: void bit(unsigned value) { if (!(position_ & 7)) bytes_.push_back(0); bytes_.back() |= (value & 1) << (7 - (position_++ & 7)); } void bits(uint32_t value, unsigned count) { check(count <= 32 && (count == 32 || value < (uint64_t(1) << count)), "AV1 bit field out of range", invalid); for (unsigned i = count; i; --i) bit(value >> (i - 1)); } void signed_bits(int value, unsigned count) { check(count && count < 32 && value >= -(1 << (count - 1)) && value < (1 << (count - 1)), "AV1 signed field out of range", invalid); bits(uint32_t(value) & ((1u << count) - 1), count); } void ns(unsigned value, unsigned count) { check(count && value < count, "AV1 non-symmetric field out of range", invalid); unsigned width = log2_ceil(count), cutoff = (1u << width) - count; if (!width) return; if (value < cutoff) bits(value, width - 1); else bits(value + cutoff, width); } void subexp(unsigned value, unsigned count) { unsigned base = 0; for (unsigned i = 0;; ++i) { unsigned width = i ? i + 2 : 3, step = 1u << width; if (count <= base + 3 * step) { ns(value - base, count - base); return; } bool more = value >= base + step; bit(more); if (!more) { bits(value - base, width); return; } base += step; } } void align() { while (position_ & 7) bit(0); } std::vector finish(bool trailing = false) { if (trailing) bit(1); align(); return std::move(bytes_); } }; void obu(std::vector &out, unsigned type, const std::vector &payload) { out.push_back(uint8_t(type << 3 | 2)); // OBU size field, no extension. size_t length = payload.size(); do { uint8_t byte = length & 0x7f; length >>= 7; out.push_back(byte | (length ? 0x80 : 0)); } while (length); out.insert(out.end(), payload.begin(), payload.end()); } std::vector sequence_header(const VADecPictureParameterBufferAV1 &p, unsigned width, unsigned height) { const auto &s = p.seq_info_fields.fields; Bits b; b.bits(0, 3); // Main profile; use the full header even for still pictures. b.bit(s.still_picture); b.bit(0); b.bit(0); // No timing or decoder model: VA supplies neither. b.bit(0); // initial_display_delay_present_flag b.bits(0, 5); // One operating point. b.bits(0, 12); unsigned level = width * height <= 2359296 ? 9 : width * height <= 8912896 ? 13 : 17; b.bits(level, 5); // Main tier, with a level appropriate for the configured dimensions. b.bit(0); unsigned width_bits = log2_ceil(width), height_bits = log2_ceil(height); b.bits(width_bits - 1, 4); b.bits(height_bits - 1, 4); b.bits(width - 1, width_bits); b.bits(height - 1, height_bits); b.bit(0); // Frame IDs are replaced by our VA-surface-to-reference mapping. b.bit(s.use_128x128_superblock); b.bit(s.enable_filter_intra); b.bit(s.enable_intra_edge_filter); b.bit(s.enable_interintra_compound); b.bit(s.enable_masked_compound); b.bit(1); // enable_warped_motion; selected per picture b.bit(s.enable_dual_filter); b.bit(s.enable_order_hint); if (s.enable_order_hint) { b.bit(s.enable_jnt_comp); b.bit(1); // enable_ref_frame_mvs; selected per picture } b.bit(1); // Screen content and integer motion selected per picture. b.bit(1); if (s.enable_order_hint) b.bits(p.order_hint_bits_minus_1, 3); b.bit(1); // enable_superres b.bit(s.enable_cdef); b.bit(1); // enable_restoration b.bit(p.bit_depth_idx == 1); b.bit(0); // mono_chrome b.bit(1); // color_description_present_flag b.bits(2, 8); // Primaries/transfer are carried by VA's output metadata. b.bits(2, 8); b.bits(p.matrix_coefficients, 8); b.bit(s.color_range); b.bits(0, 2); // chroma_sample_position is not available in the VA ABI. b.bit(1); // separate_uv_delta_q b.bit(0); // Film grain synthesis is not supported. return b.finish(true); } unsigned frame_width(const VADecPictureParameterBufferAV1 &p) { unsigned width = unsigned(p.frame_width_minus1) + 1; return p.pic_info_fields.bits.use_superres ? (width * 8 + p.superres_scale_denominator / 2) / p.superres_scale_denominator : width; } void tile_layout(Bits &b, const VADecPictureParameterBufferAV1 &p) { unsigned sb_size = p.seq_info_fields.fields.use_128x128_superblock ? 128 : 64; unsigned cols = (frame_width(p) + sb_size - 1) / sb_size; unsigned rows = (unsigned(p.frame_height_minus1) + sb_size) / sb_size; unsigned max_width = 4096 / sb_size, max_area = (4096 * 2304) / (sb_size * sb_size); unsigned min_cols_log2 = tile_log2(max_width, cols); unsigned min_tiles_log2 = std::max(min_cols_log2, tile_log2(max_area, cols * rows)); unsigned cols_log2 = log2_ceil(p.tile_cols), rows_log2 = log2_ceil(p.tile_rows); b.bit(p.pic_info_fields.bits.uniform_tile_spacing_flag); if (p.pic_info_fields.bits.uniform_tile_spacing_flag) { auto uniform = [&](unsigned blocks, unsigned count, unsigned minimum) { unsigned maximum = log2_ceil(std::min(blocks, 64u)); for (unsigned n = minimum; n <= maximum; ++n) { unsigned span = (blocks + (1u << n) - 1) >> n; if ((blocks + span - 1) / span != count) continue; for (unsigned i = minimum; i < n; ++i) b.bit(1); if (n < maximum) b.bit(0); return n; } throw Error(invalid, "invalid AV1 uniform tile layout"); }; cols_log2 = uniform(cols, p.tile_cols, min_cols_log2); rows_log2 = uniform(rows, p.tile_rows, min_tiles_log2 > cols_log2 ? min_tiles_log2 - cols_log2 : 0); } else { unsigned widest = 0, remaining = cols; for (unsigned i = 0; i < p.tile_cols; ++i) { unsigned span = i + 1 == p.tile_cols ? remaining : p.width_in_sbs_minus_1[i] + 1; check(span && span <= remaining, "invalid AV1 tile width", invalid); b.ns(span - 1, std::min(remaining, max_width)); widest = std::max(widest, span); remaining -= span; } check(!remaining, "incomplete AV1 tile columns", invalid); max_area = cols * rows; if (min_tiles_log2) max_area >>= min_tiles_log2 + 1; unsigned max_height = std::max(max_area / widest, 1u); remaining = rows; for (unsigned i = 0; i < p.tile_rows; ++i) { unsigned span = i + 1 == p.tile_rows ? remaining : p.height_in_sbs_minus_1[i] + 1; check(span && span <= remaining, "invalid AV1 tile height", invalid); b.ns(span - 1, std::min(remaining, max_height)); remaining -= span; } check(!remaining, "incomplete AV1 tile rows", invalid); } if (cols_log2 || rows_log2) { b.bits(p.context_update_tile_id, cols_log2 + rows_log2); b.bits(3, 2); // Four-byte tile lengths in the reconstructed tile group. } } void quantization(Bits &b, const VADecPictureParameterBufferAV1 &p) { auto delta = [&](int value) { b.bit(value != 0); if (value) b.signed_bits(value, 7); }; b.bits(p.base_qindex, 8); delta(p.y_dc_delta_q); bool separate = p.u_dc_delta_q != p.v_dc_delta_q || p.u_ac_delta_q != p.v_ac_delta_q; b.bit(separate); delta(p.u_dc_delta_q); delta(p.u_ac_delta_q); if (separate) { delta(p.v_dc_delta_q); delta(p.v_ac_delta_q); } const auto &q = p.qmatrix_fields.bits; b.bit(q.using_qmatrix); if (q.using_qmatrix) { b.bits(q.qm_y, 4); b.bits(q.qm_u, 4); b.bits(q.qm_v, 4); } } void segmentation(Bits &b, const VADecPictureParameterBufferAV1 &p, unsigned primary) { const auto &s = p.seg_info; const auto &flags = s.segment_info_fields.bits; b.bit(flags.enabled); if (!flags.enabled) return; if (primary != 7) { b.bit(flags.update_map); if (flags.update_map) b.bit(flags.temporal_update); b.bit(1); // VA supplies the resolved feature table, including inherited values. } constexpr unsigned widths[] = {8, 6, 6, 6, 6, 3, 0, 0}; for (unsigned segment = 0; segment < 8; ++segment) for (unsigned feature = 0; feature < 8; ++feature) { bool enabled = s.feature_mask[segment] & (1u << feature); b.bit(enabled); if (!enabled) continue; int value = s.feature_data[segment][feature]; check(value >= (feature < 5 ? -int((1u << widths[feature]) - 1) : 0) && value <= int((1u << widths[feature]) - 1), "invalid AV1 segmentation feature", invalid); if (feature < 5) b.signed_bits(value, widths[feature] + 1); else b.bits(value, widths[feature]); } } bool coded_lossless(const VADecPictureParameterBufferAV1 &p) { if (p.y_dc_delta_q || p.u_dc_delta_q || p.u_ac_delta_q || p.v_dc_delta_q || p.v_ac_delta_q) return false; for (unsigned i = 0; i < 8; ++i) { int qindex = p.base_qindex; if (p.seg_info.segment_info_fields.bits.enabled && (p.seg_info.feature_mask[i] & 1)) qindex += p.seg_info.feature_data[i][0]; if (std::clamp(qindex, 0, 255)) return false; } return true; } void filters(Bits &b, const VADecPictureParameterBufferAV1 &p, bool lossless) { if (!lossless && !p.pic_info_fields.bits.allow_intrabc) { b.bits(p.filter_level[0], 6); b.bits(p.filter_level[1], 6); if (p.filter_level[0] || p.filter_level[1]) { b.bits(p.filter_level_u, 6); b.bits(p.filter_level_v, 6); } b.bits(p.loop_filter_info_fields.bits.sharpness_level, 3); b.bit(p.loop_filter_info_fields.bits.mode_ref_delta_enabled); if (p.loop_filter_info_fields.bits.mode_ref_delta_enabled) { b.bit(1); for (int delta : p.ref_deltas) { b.bit(1); b.signed_bits(delta, 7); } for (int delta : p.mode_deltas) { b.bit(1); b.signed_bits(delta, 7); } } if (p.seq_info_fields.fields.enable_cdef) { b.bits(p.cdef_damping_minus_3, 2); b.bits(p.cdef_bits, 2); for (unsigned i = 0; i < (1u << p.cdef_bits); ++i) { b.bits(p.cdef_y_strengths[i], 6); b.bits(p.cdef_uv_strengths[i], 6); } } } if (!(lossless && !p.pic_info_fields.bits.use_superres) && !p.pic_info_fields.bits.allow_intrabc) { const auto &r = p.loop_restoration_fields.bits; constexpr unsigned syntax_type[] = {0, 2, 3, 1}; b.bits(syntax_type[r.yframe_restoration_type], 2); b.bits(syntax_type[r.cbframe_restoration_type], 2); b.bits(syntax_type[r.crframe_restoration_type], 2); if (r.yframe_restoration_type || r.cbframe_restoration_type || r.crframe_restoration_type) { check(r.lr_unit_shift <= 2, "invalid AV1 restoration unit", invalid); if (p.seq_info_fields.fields.use_128x128_superblock) { check(r.lr_unit_shift >= 1, "invalid AV1 128x128 restoration unit", invalid); b.bit(r.lr_unit_shift - 1); } else { b.bit(r.lr_unit_shift != 0); if (r.lr_unit_shift) b.bit(r.lr_unit_shift == 2); } if (r.cbframe_restoration_type || r.crframe_restoration_type) b.bit(r.lr_uv_shift); } } } std::array reference_slots(const VADecPictureParameterBufferAV1 &p, const Av1State &state) { std::array slots{}; for (unsigned i = 0; i < slots.size(); ++i) { check(p.ref_frame_idx[i] < 8, "invalid AV1 reference index", invalid); VASurfaceID id = p.ref_frame_map[p.ref_frame_idx[i]]; check(id != VA_INVALID_SURFACE, "missing AV1 reference", invalid); auto slot = std::find_if(state.references.begin(), state.references.end(), [&](const Av1Reference &r) { return r.surface == id; }); check(slot != state.references.end(), "AV1 reference is outside the decoder DPB", invalid); slots[i] = slot - state.references.begin(); } return slots; } bool skip_mode_allowed(const VADecPictureParameterBufferAV1 &p, const Av1State &state, const std::array &slots) { if (!p.seq_info_fields.fields.enable_order_hint || !p.mode_control_fields.bits.reference_select) return false; unsigned bits = p.order_hint_bits_minus_1 + 1; auto distance = [bits](int a, int b) { unsigned delta = unsigned(a - b), sign = 1u << (bits - 1); return int(delta & (sign - 1)) - int(delta & sign); }; int forward = -1, backward = -1; for (unsigned slot : slots) { int hint = state.references[slot].params.order_hint; int delta = distance(hint, p.order_hint); if (delta < 0 && (forward < 0 || distance(hint, forward) > 0)) forward = hint; if (delta > 0 && (backward < 0 || distance(hint, backward) < 0)) backward = hint; } if (forward < 0) return false; if (backward >= 0) return true; return std::any_of(slots.begin(), slots.end(), [&](unsigned slot) { return distance(state.references[slot].params.order_hint, forward) < 0; }); } unsigned recenter(unsigned reference, unsigned value) { if (value > 2 * reference) return value; return value >= reference ? 2 * (value - reference) : 2 * (reference - value) - 1; } void global_motion(Bits &b, const VADecPictureParameterBufferAV1 &p, const VADecPictureParameterBufferAV1 *primary) { for (unsigned ref = 0; ref < 7; ++ref) { const auto &motion = p.wm[ref]; unsigned type = motion.wmtype; check(type <= VAAV1TransformationAffine, "invalid AV1 global motion type", invalid); b.bit(type != VAAV1TransformationIdentity); if (!type) continue; b.bit(type == VAAV1TransformationRotzoom); if (type != VAAV1TransformationRotzoom) b.bit(type == VAAV1TransformationTranslation); auto parameter = [&](unsigned index) { unsigned absolute_bits = 12, precision = 15; if (index < 2) { bool low_precision = !p.pic_info_fields.bits.allow_high_precision_mv; absolute_bits = type == VAAV1TransformationTranslation ? 9 - low_precision : 12; precision = type == VAAV1TransformationTranslation ? 3 - low_precision : 6; } int unit = 1 << (16 - precision), identity = index % 3 == 2 ? 65536 : 0; int64_t value = (int64_t(motion.wmmat[index]) - identity) / unit; int previous = primary ? primary->wm[ref].wmmat[index] : identity; // The predictor is arithmetically shifted, including negative values. int predictor = (previous >> (16 - precision)) - identity / unit; int limit = 1 << absolute_bits; check(motion.wmmat[index] % unit == 0 && value >= -limit && value <= limit && predictor >= -limit && predictor <= limit, "invalid AV1 global motion coefficient", invalid); unsigned count = 2 * limit + 1, r = predictor + limit, v = value + limit; unsigned centered = 2 * r <= count ? recenter(r, v) : recenter(count - 1 - r, count - 1 - v); b.subexp(centered, count); }; if (type >= VAAV1TransformationRotzoom) { parameter(2); parameter(3); if (type == VAAV1TransformationAffine) { parameter(4); parameter(5); } } parameter(0); parameter(1); } } std::vector frame(const Av1Picture &picture, const Av1State &state, unsigned width, unsigned height, unsigned refresh, bool show, bool reset) { const auto &p = picture.params; const auto &f = p.pic_info_fields.bits; const auto &s = p.seq_info_fields.fields; const auto &mode = p.mode_control_fields.bits; // Hidden keys must not acquire the implicit all-slot refresh of a shown key. unsigned type = f.frame_type == key_frame && !reset ? intra_only : f.frame_type; bool intra = type == key_frame || type == intra_only; bool resilient = (type == key_frame && show) || type == switch_frame || f.error_resilient_mode; unsigned primary = intra || resilient ? 7 : p.primary_ref_frame; auto slots = intra ? std::array{} : reference_slots(p, state); Bits b; b.bit(0); // show_existing_frame is resolved by the VA client using its surface. b.bits(type, 2); b.bit(show); if (!show) b.bit(0); // Reference installation does not request a displayable picture. if (type != switch_frame && !(type == key_frame && show)) b.bit(resilient); b.bit(f.disable_cdf_update); b.bit(f.allow_screen_content_tools); if (f.allow_screen_content_tools) b.bit(f.force_integer_mv); if (type != switch_frame) b.bit(1); // Always write explicit dimensions. if (s.enable_order_hint) b.bits(p.order_hint, p.order_hint_bits_minus_1 + 1); if (!intra && !resilient) b.bits(primary, 3); if (type != switch_frame && !(type == key_frame && show)) b.bits(refresh, 8); if ((!intra || refresh != 0xff) && resilient && s.enable_order_hint) for (const auto &ref : state.references) b.bits(ref.params.order_hint, p.order_hint_bits_minus_1 + 1); if (!intra) { if (s.enable_order_hint) b.bit(0); // Explicit reference mapping, no short signaling. for (unsigned slot : slots) b.bits(slot, 3); if (!resilient) for (unsigned i = 0; i < 7; ++i) b.bit(0); // Do not inherit dimensions from a reference. } b.bits(p.frame_width_minus1, log2_ceil(width)); b.bits(p.frame_height_minus1, log2_ceil(height)); b.bit(f.use_superres); if (f.use_superres) b.bits(p.superres_scale_denominator - 9, 3); b.bit(0); // Render size matches the reconstructed VA surface. if (intra) { if (f.allow_screen_content_tools && !f.use_superres) b.bit(f.allow_intrabc); } else { if (!f.force_integer_mv) b.bit(f.allow_high_precision_mv); b.bit(p.interp_filter == 4); if (p.interp_filter != 4) b.bits(p.interp_filter, 2); b.bit(f.is_motion_mode_switchable); if (!resilient && s.enable_order_hint) b.bit(f.use_ref_frame_mvs); } if (!f.disable_cdf_update) b.bit(f.disable_frame_end_update_cdf); tile_layout(b, p); quantization(b, p); segmentation(b, p, primary); if (p.base_qindex) b.bit(mode.delta_q_present_flag); if (mode.delta_q_present_flag) { b.bits(mode.log2_delta_q_res, 2); if (!f.allow_intrabc) b.bit(mode.delta_lf_present_flag); if (mode.delta_lf_present_flag) { b.bits(mode.log2_delta_lf_res, 2); b.bit(mode.delta_lf_multi); } } bool lossless = coded_lossless(p); filters(b, p, lossless); if (!lossless) b.bit(mode.tx_mode == 2); if (!intra) b.bit(mode.reference_select); bool skip_allowed = !intra && skip_mode_allowed(p, state, slots); check(skip_allowed || !mode.skip_mode_present, "invalid AV1 skip mode references", invalid); if (skip_allowed) b.bit(mode.skip_mode_present); if (!intra && !resilient) b.bit(f.allow_warped_motion); b.bit(mode.reduced_tx_set_used); if (!intra) global_motion(b, p, primary == 7 ? nullptr : &state.references[slots[primary]].params); b.align(); if (p.tile_cols * p.tile_rows > 1) { b.bit(0); // One tile group containing every tile. b.align(); } auto payload = b.finish(); std::vector tiles(p.tile_cols * p.tile_rows); for (const auto &slice : picture.slices) { unsigned row = slice.params.tile_row, column = slice.params.tile_column; check(row < p.tile_rows && column < p.tile_cols && !tiles[row * p.tile_cols + column], "invalid or duplicate AV1 tile", invalid); tiles[row * p.tile_cols + column] = &slice; } for (unsigned i = 0; i < tiles.size(); ++i) { check(tiles[i] && !tiles[i]->bytes.empty() && tiles[i]->bytes.size() <= UINT32_MAX, "missing AV1 tile data", invalid); if (i + 1 < tiles.size()) { uint32_t length = tiles[i]->bytes.size() - 1; for (unsigned byte = 0; byte < 4; ++byte) payload.push_back(uint8_t(length >> (byte * 8))); } payload.insert(payload.end(), tiles[i]->bytes.begin(), tiles[i]->bytes.end()); } return payload; } bool referenced(const VADecPictureParameterBufferAV1 &p, VASurfaceID id) { return id != VA_INVALID_SURFACE && std::find(std::begin(p.ref_frame_map), std::end(p.ref_frame_map), id) != std::end(p.ref_frame_map); } int free_slot(const Av1State &state, const VADecPictureParameterBufferAV1 &p) { for (unsigned i = 0; i < state.references.size(); ++i) if (!referenced(p, state.references[i].surface)) return i; return -1; } void remember(Av1State &state, const Av1Picture &picture, unsigned refresh) { if (!refresh) return; bool assigned = false; for (unsigned i = 0; i < state.references.size(); ++i) { auto &ref = state.references[i]; if (ref.surface == picture.params.current_frame) ref.surface = VA_INVALID_SURFACE; if (refresh & (1u << i)) { ref.params = picture.params; // Keep a single canonical slot when the key frame refreshes all eight. ref.surface = assigned ? VA_INVALID_SURFACE : picture.params.current_frame; assigned = true; } } } } // namespace std::vector av1_bitstream(const Av1Picture &picture, Av1State &state, unsigned width, unsigned height) { check(picture.has_params && !picture.slices.empty() && picture.pending_slices.empty(), "AV1 picture missing parameters or tiles", invalid); const auto &p = picture.params; const auto &f = p.pic_info_fields.bits; const auto &s = p.seq_info_fields.fields; check(p.profile == 0 && p.bit_depth_idx <= 1 && !s.mono_chrome && s.subsampling_x && s.subsampling_y, "AV1 requires Main 8/10-bit 4:2:0", VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT); check(!f.large_scale_tile && !p.anchor_frames_num, "AV1 large scale tiles unsupported", VA_STATUS_ERROR_UNIMPLEMENTED); check(!p.film_grain_info.film_grain_info_fields.bits.apply_grain, "AV1 film grain synthesis unsupported", VA_STATUS_ERROR_UNIMPLEMENTED); check(p.current_frame != VA_INVALID_SURFACE && p.frame_width_minus1 < width && p.frame_height_minus1 < height && width >= 128 && height >= 128 && width <= 8192 && height <= 8192 && width * height <= 35651584 && (p.frame_width_minus1 & 1) && (p.frame_height_minus1 & 1), "invalid AV1 picture dimensions", invalid); check(p.tile_cols && p.tile_cols <= 64 && p.tile_rows && p.tile_rows <= 64 && picture.slices.size() == size_t(p.tile_cols) * p.tile_rows && picture.slices.size() <= 512 && p.context_update_tile_id < p.tile_cols * p.tile_rows && p.primary_ref_frame <= 7 && (!s.enable_order_hint || p.order_hint_bits_minus_1 <= 7) && p.cdef_bits <= 3, "invalid AV1 picture fields", invalid); check(!f.use_superres || (p.superres_scale_denominator >= 9 && p.superres_scale_denominator <= 16), "invalid AV1 super-resolution denominator", invalid); bool intra = f.frame_type == key_frame || f.frame_type == intra_only; check((!f.allow_intrabc || (intra && f.allow_screen_content_tools && !f.use_superres)) && (!f.force_integer_mv || intra || f.allow_screen_content_tools) && (!p.mode_control_fields.bits.delta_q_present_flag || p.base_qindex) && (!p.mode_control_fields.bits.delta_lf_present_flag || (p.mode_control_fields.bits.delta_q_present_flag && !f.allow_intrabc)), "inconsistent AV1 coding tools", invalid); auto sequence = sequence_header(p, width, height); bool new_sequence = state.sequence != sequence; check(!new_sequence || f.frame_type == key_frame, "AV1 sequence must start with a key frame", invalid); if (new_sequence) { state = Av1State{}; state.sequence = sequence; } bool reset = f.frame_type == switch_frame || (f.frame_type == key_frame && (f.show_frame || new_sequence)); std::vector out; obu(out, 2, {}); // One complete temporal unit per V4L2 OUTPUT buffer. if (new_sequence) obu(out, 1, sequence); // VA omits refresh_frame_flags. When all eight references are still live, // decode without replacing any of them. The next VA reference map tells us // which slot can be retired. Replay only that uncommitted picture as NOSHOW // before decoding the new picture, retaining its entropy and motion state. if (state.pending_reference && !reset && referenced(p, state.pending_reference->params.current_frame)) { int slot = free_slot(state, p); check(slot >= 0, "AV1 reference map exceeds eight frames", invalid); unsigned refresh = 1u << slot; obu(out, 6, frame(*state.pending_reference, state, width, height, refresh, false, false)); remember(state, *state.pending_reference, refresh); trace("AV1 install reference surface=%u slot=%d", state.pending_reference->params.current_frame, slot); } state.pending_reference.reset(); int slot = free_slot(state, p); unsigned refresh = reset ? 0xff : slot >= 0 ? 1u << slot : 0; obu(out, 6, frame(picture, state, width, height, refresh, true, reset)); remember(state, picture, refresh); if (!refresh) state.pending_reference = std::make_shared(picture); trace("AV1 frame surface=%u type=%u original-show=%u refresh=%02x tiles=%ux%u bytes=%zu", p.current_frame, f.frame_type, f.show_frame, refresh, p.tile_cols, p.tile_rows, out.size()); return out; } } // namespace irisva qcom-va-driver-1.1.0/src/backend.cpp000066400000000000000000002065571526764642600172700ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "internal.hpp" #include #include #include #include #include #include namespace irisva { Buffer::~Buffer() { if (acquired_fd >= 0) { close(acquired_fd); --parent->acquired_handles; } if (mapped) { dma_buf_sync sync{DMA_BUF_SYNC_END | DMA_BUF_SYNC_RW}; ioctl(memory->fd, DMA_BUF_IOCTL_SYNC, &sync); --parent->mapped_images; } if (memory) --parent->derived_images; } uint8_t *Buffer::map() { if (type == VAEncCodedBufferType) { check(coded_ready, "coded buffer is not ready", VA_STATUS_ERROR_OPERATION_FAILED); coded_mapped = true; return reinterpret_cast(&coded); } if (!memory) return data.data(); check(!parent->acquired_handles, "surface handle acquired", VA_STATUS_ERROR_SURFACE_BUSY); auto pointer = memory->map(); if (!mapped) { dma_buf_sync sync{DMA_BUF_SYNC_START | DMA_BUF_SYNC_RW}; check(ioctl(memory->fd, DMA_BUF_IOCTL_SYNC, &sync) == 0, "DMA-BUF map synchronization failed"); mapped = true; ++parent->mapped_images; } return pointer; } void Buffer::unmap() { coded_mapped = false; if (!mapped) return; dma_buf_sync sync{DMA_BUF_SYNC_END | DMA_BUF_SYNC_RW}; check(ioctl(memory->fd, DMA_BUF_IOCTL_SYNC, &sync) == 0, "DMA-BUF unmap synchronization failed"); mapped = false; --parent->mapped_images; } struct ImageMapping { std::shared_ptr buffer; bool was_mapped; uint8_t *data; explicit ImageMapping(std::shared_ptr b) : buffer(std::move(b)), was_mapped(buffer->mapped), data(buffer->map()) {} void finish() { if (!was_mapped) buffer->unmap(); } ~ImageMapping() { try { finish(); } catch (...) { } } }; template VAStatus guard(VADriverContextP ctx, F f) noexcept { try { auto &d = *static_cast(ctx->pDriverData); std::lock_guard lock(d.mutex); return f(d); } catch (const Error &e) { std::fprintf(stderr, "iris-vaapi: %s (VA status %#x)\n", e.what(), e.status); return e.status; } catch (const std::bad_alloc &) { return VA_STATUS_ERROR_ALLOCATION_FAILED; } catch (const std::exception &e) { std::fprintf(stderr, "iris-vaapi: %s\n", e.what()); return VA_STATUS_ERROR_OPERATION_FAILED; } } #define API(name, args, ...) \ static VAStatus name args { \ return guard(ctx, [&](Driver &d) -> VAStatus { __VA_ARGS__ }); \ } static bool supported(const Driver &d, VAProfile p) { return p == VAProfileH264ConstrainedBaseline || p == VAProfileH264Main || p == VAProfileH264High || is_hevc(p) || is_vp9(p) || (is_av1(p) && d.av1); } static bool encode_supported(const Driver &d, VAProfile p, VAEntrypoint e) { return !d.encoder_device.empty() && e == VAEntrypointEncSlice && (p == VAProfileH264ConstrainedBaseline || p == VAProfileH264Main || p == VAProfileH264High || p == VAProfileHEVCMain); } static unsigned encode_attribute(VAConfigAttribType type, VAProfile profile) { (void)profile; // HEVC attributes are only available in libva >= 1.13. switch (type) { #if VA_CHECK_VERSION(1, 13, 0) case VAConfigAttribEncHEVCFeatures: { if (profile != VAProfileHEVCMain) return VA_ATTRIB_NOT_SUPPORTED; // Iris generates the parameter sets and chooses coding tools itself. VAConfigAttribValEncHEVCFeatures features{}; features.bits.sao = VA_FEATURE_REQUIRED; features.bits.temporal_mvp = VA_FEATURE_REQUIRED; features.bits.cu_qp_delta = VA_FEATURE_SUPPORTED; features.bits.deblocking_filter_disable = VA_FEATURE_SUPPORTED; return features.value; } case VAConfigAttribEncHEVCBlockSizes: { if (profile != VAProfileHEVCMain) return VA_ATTRIB_NOT_SUPPORTED; VAConfigAttribValEncHEVCBlockSizes sizes{}; sizes.bits.log2_max_coding_tree_block_size_minus3 = 2; sizes.bits.log2_min_coding_tree_block_size_minus3 = 2; sizes.bits.log2_max_luma_transform_block_size_minus2 = 3; return sizes.value; } #endif case VAConfigAttribRTFormat: return VA_RT_FORMAT_YUV420; case VAConfigAttribRateControl: return VA_RC_CQP | VA_RC_CBR | VA_RC_VBR; case VAConfigAttribEncPackedHeaders: return VA_ENC_PACKED_HEADER_NONE; case VAConfigAttribEncInterlaced: return VA_ENC_INTERLACED_NONE; case VAConfigAttribEncMaxRefFrames: return 1; // One past reference, no future references. case VAConfigAttribEncMaxSlices: return 1; case VAConfigAttribEncSliceStructure: return VA_ENC_SLICE_STRUCTURE_EQUAL_ROWS; case VAConfigAttribEncQualityRange: return 1; case VAConfigAttribPredictionDirection: return VA_PREDICTION_DIRECTION_PREVIOUS; case VAConfigAttribMaxPictureWidth: return 3840; case VAConfigAttribMaxPictureHeight: return 2160; default: return VA_ATTRIB_NOT_SUPPORTED; } } static unsigned profile_format(VAProfile p) { if (p == VAProfileVP9Profile2) return VA_RT_FORMAT_YUV420_10; // HEVC Main10 and AV1 Main accept both 8-bit and 10-bit surfaces. if (p == VAProfileHEVCMain10 || is_av1(p)) return VA_RT_FORMAT_YUV420 | VA_RT_FORMAT_YUV420_10; return VA_RT_FORMAT_YUV420; } static unsigned render_formats(VAProfile profile, VAEntrypoint entrypoint) { return entrypoint == VAEntrypointVideoProc ? (VA_RT_FORMAT_YUV420 | VA_RT_FORMAT_YUV420_10 | VA_RT_FORMAT_RGB32) : profile_format(profile); } static unsigned profile_fourcc(VAProfile p) { return is_10bit(p) || is_av1(p) ? VA_FOURCC_P010 : VA_FOURCC_NV12; } static auto surface(Driver &d, VASurfaceID id) { return lookup(d.surfaces, id, VA_STATUS_ERROR_INVALID_SURFACE); } static auto context(Driver &d, VAContextID id) { return lookup(d.contexts, id, VA_STATUS_ERROR_INVALID_CONTEXT); } static auto buffer(Driver &d, VABufferID id) { return lookup(d.buffers, id, VA_STATUS_ERROR_INVALID_BUFFER); } static VAStatus synchronize_encode(const std::shared_ptr &task, uint64_t timeout = VA_TIMEOUT_INFINITE) { if (!task) return VA_STATUS_SUCCESS; if (task->pending) { auto encoder = task->encoder.lock(); check(bool(encoder), "surface encoder no longer exists", VA_STATUS_ERROR_INVALID_CONTEXT); if (!encoder->sync(task, timeout)) return VA_STATUS_ERROR_TIMEDOUT; } check(task->status == VA_STATUS_SUCCESS, "asynchronous encoding failed", task->status); return VA_STATUS_SUCCESS; } static void synchronize(Driver &d, const std::shared_ptr &s) { if (auto decoder = s->decoder.lock()) { // Keep the decoder and surface alive while allowing other VA submissions. d.mutex.unlock(); try { decoder->sync(s); } catch (...) { d.mutex.lock(); throw; } d.mutex.lock(); } synchronize_encode(s->encode_task); check(!s->pending, "surface decoder no longer exists", VA_STATUS_ERROR_INVALID_CONTEXT); if (s->status) throw Error(s->status, "surface contains a decoding error"); } static VAImageFormat image_format(unsigned fourcc) { VAImageFormat f{}; f.fourcc = fourcc; f.byte_order = VA_LSB_FIRST; f.bits_per_pixel = fourcc == VA_FOURCC_P010 ? 24 : 12; if (is_rgb(fourcc)) { f.bits_per_pixel = f.depth = 32; f.red_mask = fourcc == VA_FOURCC_BGRA ? 0xff0000 : 0xff; f.green_mask = 0xff00; f.blue_mask = fourcc == VA_FOURCC_BGRA ? 0xff : 0xff0000; f.alpha_mask = 0xff000000; } return f; } static VAImage create_image(Driver &d, unsigned fourcc, unsigned w, unsigned h) { check(w && h && w <= 8192 && h <= 8192, "invalid image dimensions", VA_STATUS_ERROR_INVALID_PARAMETER); check(fourcc == VA_FOURCC_NV12 || fourcc == VA_FOURCC_I420 || fourcc == VA_FOURCC_P010 || is_rgb(fourcc), "unsupported image format", VA_STATUS_ERROR_INVALID_IMAGE_FORMAT); VAImage image{}; image.image_id = d.next_id++; image.buf = d.next_id++; image.format = image_format(fourcc); image.width = w; image.height = h; image.pitches[0] = ((w + 1) & ~1u) * (fourcc == VA_FOURCC_P010 ? 2 : 1); image.offsets[1] = image.pitches[0] * h; unsigned chroma_h = (h + 1) / 2; if (is_rgb(fourcc)) { image.num_planes = 1; image.pitches[0] = w * 4; image.offsets[1] = 0; image.data_size = image.pitches[0] * h; } else if (fourcc != VA_FOURCC_I420) { image.num_planes = 2; image.pitches[1] = image.pitches[0]; image.data_size = image.offsets[1] + image.pitches[1] * chroma_h; } else { image.num_planes = 3; image.pitches[1] = image.pitches[2] = image.pitches[0] / 2; image.offsets[2] = image.offsets[1] + image.pitches[1] * chroma_h; image.data_size = image.offsets[2] + image.pitches[2] * chroma_h; } auto b = std::make_shared(); b->type = VAImageBufferType; b->element_size = image.data_size; b->elements = 1; b->data.resize(image.data_size); d.buffers[image.buf] = b; d.images[image.image_id] = image; return image; } static std::shared_ptr allocate_undecoded_surface(VADriverContextP ctx, const Surface &s) { // mpv probes GPU import before creating a decoder context. Use an actual // linear MSM GEM allocation for these otherwise unbacked VA surfaces. // The PRIME fd owns the storage after the temporary GEM handle is closed. // If exported before decode, keep this allocation for the surface's // lifetime. A consumer may keep its fd without calling export again. check(ctx->drm_state, "surface allocation requires a DRM display"); int fd = static_cast(ctx->drm_state)->fd; return allocate_surface(fd, s.width, s.height, s.fourcc); } API(query_profiles, (VADriverContextP ctx, VAProfile *out, int *count), (void)d; check(out && count, "null profiles", VA_STATUS_ERROR_INVALID_PARAMETER); out[0] = VAProfileH264ConstrainedBaseline; out[1] = VAProfileH264Main; out[2] = VAProfileH264High; out[3] = VAProfileHEVCMain; out[4] = VAProfileHEVCMain10; out[5] = VAProfileVP9Profile0; out[6] = VAProfileVP9Profile2; *count = 7; if (d.av1) out[(*count)++] = VAProfileAV1Profile0; if (vpp_supported(VAProfileNone, VAEntrypointVideoProc)) out[(*count)++] = VAProfileNone; return VA_STATUS_SUCCESS;) API(query_entrypoints, (VADriverContextP ctx, VAProfile profile, VAEntrypoint *out, int *count), (void)d; check(supported(d, profile) || vpp_supported(profile, VAEntrypointVideoProc), "unsupported profile", VA_STATUS_ERROR_UNSUPPORTED_PROFILE); check(out && count, "null entrypoints", VA_STATUS_ERROR_INVALID_PARAMETER); out[0] = profile == VAProfileNone ? VAEntrypointVideoProc : VAEntrypointVLD; *count = 1; if (encode_supported(d, profile, VAEntrypointEncSlice)) out[(*count)++] = VAEntrypointEncSlice; return VA_STATUS_SUCCESS;) API( get_attributes, (VADriverContextP ctx, VAProfile profile, VAEntrypoint entry, VAConfigAttrib *attrs, int count), (void)d; check(supported(d, profile) || vpp_supported(profile, VAEntrypointVideoProc), "unsupported profile", VA_STATUS_ERROR_UNSUPPORTED_PROFILE); check((supported(d, profile) && entry == VAEntrypointVLD) || vpp_supported(profile, entry) || encode_supported(d, profile, entry), "unsupported entrypoint", VA_STATUS_ERROR_UNSUPPORTED_ENTRYPOINT); for (int i = 0; i < count; ++i) { if (entry == VAEntrypointVideoProc) { attrs[i].value = attrs[i].type == VAConfigAttribRTFormat ? render_formats(profile, entry) : (attrs[i].type == VAConfigAttribMaxPictureWidth || attrs[i].type == VAConfigAttribMaxPictureHeight) ? 8192 : VA_ATTRIB_NOT_SUPPORTED; continue; } if (entry == VAEntrypointEncSlice) { attrs[i].value = encode_attribute(attrs[i].type, profile); continue; } switch (attrs[i].type) { case VAConfigAttribRTFormat: attrs[i].value = profile_format(profile); break; case VAConfigAttribDecAV1Features: attrs[i].value = is_av1(profile) ? 0 : VA_ATTRIB_NOT_SUPPORTED; break; case VAConfigAttribDecSliceMode: attrs[i].value = VA_DEC_SLICE_MODE_NORMAL; break; case VAConfigAttribMaxPictureWidth: case VAConfigAttribMaxPictureHeight: attrs[i].value = 8192; break; default: attrs[i].value = VA_ATTRIB_NOT_SUPPORTED; } } return VA_STATUS_SUCCESS;) API( create_config, (VADriverContextP ctx, VAProfile p, VAEntrypoint e, VAConfigAttrib *attrs, int count, VAConfigID *id), check(supported(d, p) || vpp_supported(p, VAEntrypointVideoProc), "unsupported profile", VA_STATUS_ERROR_UNSUPPORTED_PROFILE); check((supported(d, p) && e == VAEntrypointVLD) || vpp_supported(p, e) || encode_supported(d, p, e), "unsupported entrypoint", VA_STATUS_ERROR_UNSUPPORTED_ENTRYPOINT); Config config{p, e, VA_RC_CQP}; check(id, "null config ID", VA_STATUS_ERROR_INVALID_PARAMETER); for (int i = 0; i < count; ++i) { if (e == VAEntrypointEncSlice) { const auto type = attrs[i].type; check(type == VAConfigAttribRTFormat || type == VAConfigAttribRateControl || type == VAConfigAttribEncPackedHeaders || type == VAConfigAttribEncInterlaced, "unsupported encoder config attribute", VA_STATUS_ERROR_ATTR_NOT_SUPPORTED); auto value = encode_attribute(type, p); check(!(attrs[i].value & ~value) && (type != VAConfigAttribRTFormat || attrs[i].value == VA_RT_FORMAT_YUV420), "unsupported encoder attribute", VA_STATUS_ERROR_ATTR_NOT_SUPPORTED); if (attrs[i].type == VAConfigAttribRateControl) { check(attrs[i].value == VA_RC_CQP || attrs[i].value == VA_RC_CBR || attrs[i].value == VA_RC_VBR, "select one encoder rate control mode", VA_STATUS_ERROR_ATTR_NOT_SUPPORTED); config.rate_control = attrs[i].value; } } else if (attrs[i].type == VAConfigAttribRTFormat) check(attrs[i].value && !(attrs[i].value & ~render_formats(p, e)), "unsupported render format", VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT); else if (e == VAEntrypointVLD && attrs[i].type == VAConfigAttribDecSliceMode) check(attrs[i].value == VA_DEC_SLICE_MODE_NORMAL, "unsupported slice mode", VA_STATUS_ERROR_ATTR_NOT_SUPPORTED); else if (e == VAEntrypointVLD && is_av1(p) && attrs[i].type == VAConfigAttribDecAV1Features) check(!attrs[i].value, "AV1 large scale tiles unsupported", VA_STATUS_ERROR_ATTR_NOT_SUPPORTED); else throw Error(VA_STATUS_ERROR_ATTR_NOT_SUPPORTED, "unsupported config attribute"); } *id = d.next_id++; d.configs[*id] = config; return VA_STATUS_SUCCESS;) API(destroy_config, (VADriverContextP ctx, VAConfigID id), check(d.configs.erase(id), "invalid config", VA_STATUS_ERROR_INVALID_CONFIG); return VA_STATUS_SUCCESS;) API(query_config, (VADriverContextP ctx, VAConfigID id, VAProfile *p, VAEntrypoint *e, VAConfigAttrib *a, int *n), auto config = lookup(d.configs, id, VA_STATUS_ERROR_INVALID_CONFIG); *p = config.profile; *e = config.entrypoint; a[0] = {VAConfigAttribRTFormat, render_formats(*p, *e)}; *n = 1; if (*e == VAEntrypointEncSlice) a[(*n)++] = {VAConfigAttribRateControl, config.rate_control}; return VA_STATUS_SUCCESS;) API( query_surfaces, (VADriverContextP ctx, VAConfigID id, VASurfaceAttrib *attrs, unsigned *count), auto config = lookup(d.configs, id, VA_STATUS_ERROR_INVALID_CONFIG); auto profile = config.profile; check(count, "null surface attribute count", VA_STATUS_ERROR_INVALID_PARAMETER); VASurfaceAttrib a[9]{}; VASurfaceAttribType types[] = {VASurfaceAttribPixelFormat, VASurfaceAttribMinWidth, VASurfaceAttribMinHeight, VASurfaceAttribMaxWidth, VASurfaceAttribMaxHeight, VASurfaceAttribMemoryType}; unsigned values[] = {profile_fourcc(profile), 128, 128, 8192, 8192, VA_SURFACE_ATTRIB_MEM_TYPE_VA | VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME | VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2}; if (config.entrypoint == VAEntrypointVideoProc) { values[1] = 2; values[2] = 2; } if (config.entrypoint == VAEntrypointEncSlice) { values[3] = 3840; values[4] = 2160; } for (unsigned i = 0; i < 6; ++i) { a[i].type = types[i]; a[i].flags = VA_SURFACE_ATTRIB_GETTABLE; if (i == 0 || i == 5) a[i].flags |= VA_SURFACE_ATTRIB_SETTABLE; a[i].value.type = VAGenericValueTypeInteger; a[i].value.value.i = values[i]; } unsigned n = 6; if (config.entrypoint == VAEntrypointVideoProc) { a[n] = a[0]; a[n++].value.value.i = VA_FOURCC_P010; a[n] = a[0]; a[n++].value.value.i = VA_FOURCC_BGRA; a[n] = a[0]; a[n++].value.value.i = VA_FOURCC_RGBA; } else if (values[0] != VA_FOURCC_NV12 && (profile_format(profile) & VA_RT_FORMAT_YUV420)) { a[n] = a[0]; a[n++].value.value.i = VA_FOURCC_NV12; } if (!attrs) { *count = n; return VA_STATUS_SUCCESS; } if (*count < n) { *count = n; return VA_STATUS_ERROR_MAX_NUM_EXCEEDED; } std::memcpy(attrs, a, n * sizeof(*a)); *count = n; return VA_STATUS_SUCCESS;) // Normalize the legacy descriptor before applying the shared DMA-BUF checks. // PRIME1 has no explicit modifier, so only unflagged linear buffers are accepted. static VADRMPRIMESurfaceDescriptor prime_descriptor(const VASurfaceAttribExternalBuffers &external) { bool rgb = is_rgb(external.pixel_format); check(external.num_buffers == 1 && external.buffers && external.buffers[0] <= INT_MAX && external.num_planes == (rgb ? 1u : 2u) && !external.flags, "PRIME1 requires one linear DMA-BUF", VA_STATUS_ERROR_INVALID_PARAMETER); VADRMPRIMESurfaceDescriptor desc{}; desc.fourcc = external.pixel_format; desc.width = external.width; desc.height = external.height; desc.num_objects = desc.num_layers = 1; desc.objects[0].fd = static_cast(external.buffers[0]); desc.objects[0].size = external.data_size; desc.objects[0].drm_format_modifier = DRM_FORMAT_MOD_LINEAR; auto &layer = desc.layers[0]; layer.drm_format = external.pixel_format == VA_FOURCC_P010 ? DRM_FORMAT_P010 : DRM_FORMAT_NV12; if (rgb) layer.drm_format = external.pixel_format == VA_FOURCC_BGRA ? DRM_FORMAT_ARGB8888 : DRM_FORMAT_ABGR8888; layer.num_planes = rgb ? 1 : 2; for (unsigned i = 0; i < layer.num_planes; ++i) { layer.offset[i] = external.offsets[i]; layer.pitch[i] = external.pitches[i]; } return desc; } static std::shared_ptr import_prime(const VADRMPRIMESurfaceDescriptor &desc, unsigned width, unsigned height, unsigned fourcc) { check(desc.fourcc == fourcc && desc.width == width && desc.height == height && desc.num_objects == 1 && desc.objects[0].fd >= 0 && desc.objects[0].drm_format_modifier == DRM_FORMAT_MOD_LINEAR, "import requires matching single-object linear surface", VA_STATUS_ERROR_INVALID_PARAMETER); bool p010 = fourcc == VA_FOURCC_P010; unsigned offsets[2]{}, pitches[2]{}; bool rgb = is_rgb(fourcc); if (rgb) { const auto &l = desc.layers[0]; check(desc.num_layers == 1 && l.num_planes == 1 && !l.object_index[0] && l.drm_format == (fourcc == VA_FOURCC_BGRA ? DRM_FORMAT_ARGB8888 : DRM_FORMAT_ABGR8888) && !(l.offset[0] & 3) && !(l.pitch[0] & 3) && l.pitch[0] >= width * 4 && uint64_t(l.offset[0]) + uint64_t(height - 1) * l.pitch[0] + width * 4 <= desc.objects[0].size, "invalid RGB DMA-BUF layout", VA_STATUS_ERROR_INVALID_PARAMETER); offsets[0] = l.offset[0]; pitches[0] = l.pitch[0]; } else if (desc.num_layers == 1) { const auto &l = desc.layers[0]; check(l.drm_format == (p010 ? DRM_FORMAT_P010 : DRM_FORMAT_NV12) && l.num_planes == 2 && !l.object_index[0] && !l.object_index[1], "invalid composed DMA-BUF layer", VA_STATUS_ERROR_INVALID_PARAMETER); for (unsigned i = 0; i < 2; ++i) { offsets[i] = l.offset[i]; pitches[i] = l.pitch[i]; } } else { check(desc.num_layers == 2 && desc.layers[0].drm_format == (p010 ? DRM_FORMAT_R16 : DRM_FORMAT_R8) && desc.layers[1].drm_format == (p010 ? DRM_FORMAT_GR1616 : DRM_FORMAT_GR88), "invalid separate DMA-BUF layers", VA_STATUS_ERROR_INVALID_PARAMETER); for (unsigned i = 0; i < 2; ++i) { const auto &l = desc.layers[i]; check(l.num_planes == 1 && !l.object_index[0], "invalid DMA-BUF plane", VA_STATUS_ERROR_INVALID_PARAMETER); offsets[i] = l.offset[0]; pitches[i] = l.pitch[0]; } } unsigned sample_bytes = p010 ? 2 : 1; unsigned row_bytes = ((width + 1) & ~1u) * sample_bytes; check(rgb || (pitches[0] >= row_bytes && pitches[0] == pitches[1] && pitches[0] % sample_bytes == 0 && offsets[0] % sample_bytes == 0 && offsets[1] >= offsets[0] && (offsets[1] - offsets[0]) % pitches[0] == 0 && (offsets[1] - offsets[0]) / pitches[0] >= height && uint64_t(offsets[1]) + uint64_t((height + 1) / 2 - 1) * pitches[1] + row_bytes <= desc.objects[0].size), "unsupported DMA-BUF plane layout", VA_STATUS_ERROR_INVALID_PARAMETER); auto m = std::make_shared(); m->fd = fcntl(desc.objects[0].fd, F_DUPFD_CLOEXEC, 0); check(m->fd >= 0, "duplicate imported DMA-BUF"); // DMA-BUF llseek reports the real allocation length. Do not trust a descriptor // that would let a subsequent mmap/DSP access extend beyond the allocation. auto size = lseek(m->fd, 0, SEEK_END); check(size >= 0 && uint64_t(size) >= desc.objects[0].size, "DMA-BUF allocation smaller than descriptor", VA_STATUS_ERROR_INVALID_PARAMETER); m->size = desc.objects[0].size; m->fourcc = fourcc; m->width = width; m->height = height; m->stride = pitches[0]; m->storage_height = rgb ? height : (offsets[1] - offsets[0]) / pitches[0]; m->data_offset = offsets[0]; m->origin = MemoryOrigin::Imported; return m; } API( create_surfaces, (VADriverContextP ctx, unsigned format, unsigned w, unsigned h, VASurfaceID *ids, unsigned count, VASurfaceAttrib *attrs, unsigned nattrs), check(format == VA_RT_FORMAT_YUV420 || format == VA_RT_FORMAT_YUV420_10 || format == VA_RT_FORMAT_RGB32, "unsupported surface RT format", VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT); check(w && h && w <= 8192 && h <= 8192 && ids && count <= 256, "invalid surfaces", VA_STATUS_ERROR_INVALID_PARAMETER); check(!nattrs || attrs, "null surface attributes", VA_STATUS_ERROR_INVALID_PARAMETER); unsigned fourcc = format == VA_RT_FORMAT_RGB32 ? VA_FOURCC_BGRA : format == VA_RT_FORMAT_YUV420_10 ? VA_FOURCC_P010 : VA_FOURCC_NV12; unsigned memory_type = VA_SURFACE_ATTRIB_MEM_TYPE_VA; const void *external = nullptr; bool explicit_modifier = false; for (unsigned i = 0; i < nattrs; ++i) { if (!(attrs[i].flags & VA_SURFACE_ATTRIB_SETTABLE)) continue; #if VA_CHECK_VERSION(1, 13, 0) if (attrs[i].type == VASurfaceAttribDRMFormatModifiers) { check(attrs[i].value.type == VAGenericValueTypePointer && attrs[i].value.value.p, "invalid modifier list", VA_STATUS_ERROR_INVALID_PARAMETER); const auto &list = *static_cast(attrs[i].value.value.p); check(list.num_modifiers && list.modifiers && std::find(list.modifiers, list.modifiers + list.num_modifiers, DRM_FORMAT_MOD_LINEAR) != list.modifiers + list.num_modifiers, "only linear surface allocation is supported", VA_STATUS_ERROR_ATTR_NOT_SUPPORTED); explicit_modifier = true; continue; } #endif if (attrs[i].type == VASurfaceAttribExternalBufferDescriptor) { check(attrs[i].value.type == VAGenericValueTypePointer && attrs[i].value.value.p, "invalid external descriptor", VA_STATUS_ERROR_INVALID_PARAMETER); external = attrs[i].value.value.p; continue; } check(attrs[i].value.type == VAGenericValueTypeInteger, "unsupported surface attribute", VA_STATUS_ERROR_ATTR_NOT_SUPPORTED); if (attrs[i].type == VASurfaceAttribPixelFormat) { unsigned requested = attrs[i].value.value.i; check(format == VA_RT_FORMAT_RGB32 ? is_rgb(requested) : requested == fourcc, "surface pixel format does not match RT format", VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT); fourcc = requested; } else if (attrs[i].type == VASurfaceAttribMemoryType) memory_type = attrs[i].value.value.i; else if (attrs[i].type != VASurfaceAttribUsageHint) throw Error(VA_STATUS_ERROR_ATTR_NOT_SUPPORTED, "unsupported surface attribute"); } check(memory_type == VA_SURFACE_ATTRIB_MEM_TYPE_VA || memory_type == VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME || memory_type == VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, "unsupported surface memory type", VA_STATUS_ERROR_UNSUPPORTED_MEMORY_TYPE); std::shared_ptr imported; if (memory_type != VA_SURFACE_ATTRIB_MEM_TYPE_VA) { check(external && !explicit_modifier && count == 1, "PRIME import requires one descriptor/surface", VA_STATUS_ERROR_INVALID_PARAMETER); auto desc = memory_type == VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME ? prime_descriptor(*static_cast(external)) : *static_cast(external); imported = import_prime(desc, w, h, fourcc); } else check(!external, "external descriptor without PRIME memory type", VA_STATUS_ERROR_INVALID_PARAMETER); for (unsigned i = 0; i < count; ++i) { auto s = std::make_shared(); s->memory = imported; s->persistent_export = bool(imported); s->width = w; s->height = h; s->allocation_count = count; s->fourcc = fourcc; ids[i] = d.next_id++; d.surfaces[ids[i]] = s; } return VA_STATUS_SUCCESS;) API(create_surfaces_legacy, (VADriverContextP ctx, int w, int h, int format, int n, VASurfaceID *ids), (void)d; return create_surfaces(ctx, format, w, h, ids, n, nullptr, 0);) API(destroy_surfaces, (VADriverContextP ctx, VASurfaceID *ids, int count), for (int i = 0; i < count; ++i) check(!surface(d, ids[i])->acquired_handles, "surface handle acquired", VA_STATUS_ERROR_SURFACE_BUSY); for (int i = 0; i < count; ++i) d.surfaces.erase(ids[i]); return VA_STATUS_SUCCESS;) API( create_context, (VADriverContextP ctx, VAConfigID id, int w, int h, int flags, VASurfaceID *targets, int count, VAContextID *result), (void)flags; auto config = lookup(d.configs, id, VA_STATUS_ERROR_INVALID_CONFIG); bool processing = config.entrypoint == VAEntrypointVideoProc; check(((processing && w >= 0 && h >= 0) || (w >= 128 && h >= 128)) && w <= 8192 && h <= 8192 && result, "invalid context dimensions", VA_STATUS_ERROR_RESOLUTION_NOT_SUPPORTED); auto c = std::make_shared(); c->profile = config.profile; c->entrypoint = config.entrypoint; c->encode_settings.rate_control = config.rate_control; c->encode_settings.hevc = is_hevc(c->profile); if (c->entrypoint == VAEntrypointEncSlice) check(w <= 3840 && h <= 2160 && !(w & 1) && !(h & 1), "unsupported encoder dimensions", VA_STATUS_ERROR_RESOLUTION_NOT_SUPPORTED); for (int i = 0; i < count; ++i) surface(d, targets[i]); c->width = w; c->height = h; c->internal_profile = c->profile; c->decode_surfaces = std::max(0, count); if (processing) { check(ctx->drm_state, "VPP requires a DRM display", VA_STATUS_ERROR_INVALID_DISPLAY); c->vpp = std::make_unique(static_cast(ctx->drm_state)->fd); } *result = d.next_id++; d.contexts[*result] = c; return VA_STATUS_SUCCESS;) API( destroy_context, (VADriverContextP ctx, VAContextID id), auto c = context(d, id); d.contexts.erase(id); if (c->encoder) c->encoder->finish(); if (!c->decoder) return VA_STATUS_SUCCESS; // FFmpeg can destroy its decoder context while downstream still owns the // last VA frames. Complete pending pictures before closing the V4L2 session; // exported DMA-BUF storage remains owned by those surfaces afterwards. d.mutex.unlock(); try { c->decoder->finish(); } catch (...) { d.mutex.lock(); throw; } d.mutex.lock(); return VA_STATUS_SUCCESS;) API(create_buffer, (VADriverContextP ctx, VAContextID id, VABufferType type, unsigned size, unsigned count, void *data, VABufferID *result), auto c = context(d, id); check(result && size && count && uint64_t(size) * count <= 64 * 1024 * 1024, "invalid buffer size", VA_STATUS_ERROR_INVALID_PARAMETER); bool encoding = c->entrypoint == VAEntrypointEncSlice; check(c->entrypoint == VAEntrypointVideoProc ? (type == VAProcPipelineParameterBufferType || type == VAProcFilterParameterBufferType) : encoding ? (type == VAEncSequenceParameterBufferType || type == VAEncPictureParameterBufferType || type == VAEncSliceParameterBufferType || type == VAEncMiscParameterBufferType || type == VAEncCodedBufferType) : (type == VAPictureParameterBufferType || type == VAIQMatrixBufferType || type == VASliceParameterBufferType || type == VASliceDataBufferType), "unsupported buffer type", VA_STATUS_ERROR_UNSUPPORTED_BUFFERTYPE); auto b = std::make_shared(); b->context_id = id; b->type = type; b->element_size = size; b->elements = count; b->data.resize(size_t(size) * count); if (data) std::memcpy(b->data.data(), data, b->data.size()); *result = d.next_id++; d.buffers[*result] = b; return VA_STATUS_SUCCESS;) API(set_elements, (VADriverContextP ctx, VABufferID id, unsigned count), auto b = buffer(d, id); check(uint64_t(b->element_size) * count <= b->data.size(), "buffer elements exceed allocation", VA_STATUS_ERROR_INVALID_PARAMETER); b->elements = count; return VA_STATUS_SUCCESS;) API(map_buffer, (VADriverContextP ctx, VABufferID id, void **data), check(data, "null mapped pointer", VA_STATUS_ERROR_INVALID_PARAMETER); auto b = buffer(d, id); synchronize_encode(b->encode_task); *data = b->map(); return VA_STATUS_SUCCESS;) API(sync_buffer, (VADriverContextP ctx, VABufferID id, uint64_t timeout), if (d.buffers.find(id) == d.buffers.end()) return VA_STATUS_ERROR_INVALID_BUFFER; auto b = buffer(d, id); check(b->type == VAEncCodedBufferType, "only coded buffers can be synchronized", VA_STATUS_ERROR_UNSUPPORTED_BUFFERTYPE); auto status = synchronize_encode(b->encode_task, timeout); if (status != VA_STATUS_SUCCESS) return status; check(b->coded_ready, "coded buffer has no completed picture", VA_STATUS_ERROR_OPERATION_FAILED); return VA_STATUS_SUCCESS;) API(unmap_buffer, (VADriverContextP ctx, VABufferID id), buffer(d, id)->unmap(); return VA_STATUS_SUCCESS;) API(destroy_buffer, (VADriverContextP ctx, VABufferID id), check(buffer(d, id)->acquired_fd < 0, "buffer handle acquired", VA_STATUS_ERROR_SURFACE_BUSY); check(d.buffers.erase(id), "invalid buffer", VA_STATUS_ERROR_INVALID_BUFFER); return VA_STATUS_SUCCESS;) API(buffer_info, (VADriverContextP ctx, VABufferID id, VABufferType *type, unsigned *size, unsigned *n), auto b = buffer(d, id); *type = b->type; *size = b->element_size; *n = b->elements; return VA_STATUS_SUCCESS;) API( begin_picture, (VADriverContextP ctx, VAContextID id, VASurfaceID target), auto c = context(d, id); check(!c->target, "picture already open", VA_STATUS_ERROR_OPERATION_FAILED); auto s = surface(d, target); // Main10 and AV1 permit NV12 and P010. Select the decoder's bit depth // from the first target, including clients that create a context without // render targets. Keep it fixed for the lifetime of the decoder. if (c->entrypoint == VAEntrypointVLD && c->profile == VAProfileHEVCMain10 && !c->decoder) c->internal_profile = s->fourcc == VA_FOURCC_NV12 ? VAProfileHEVCMain : VAProfileHEVCMain10; if (c->entrypoint == VAEntrypointVLD && !c->decoder) { c->decode_fourcc = is_av1(c->profile) ? s->fourcc : profile_fourcc(c->internal_profile); check(c->decode_fourcc == VA_FOURCC_NV12 || c->decode_fourcc == VA_FOURCC_P010, "unsupported decoder surface format", VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT); } check(c->entrypoint == VAEntrypointVideoProc ? (s->fourcc == VA_FOURCC_NV12 || s->fourcc == VA_FOURCC_P010) : s->fourcc == (c->entrypoint == VAEntrypointVLD ? c->decode_fourcc : profile_fourcc(c->internal_profile)), "surface format does not match context", VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT); // Defer decoder creation until the target surface format is known. if (c->entrypoint == VAEntrypointVLD && !c->decoder) { unsigned rt_format = s->fourcc == VA_FOURCC_NV12 ? VA_RT_FORMAT_YUV420 : VA_RT_FORMAT_YUV420_10; int render_fd = ctx->drm_state ? static_cast(ctx->drm_state)->fd : -1; c->decoder = std::make_shared(d.device, c->width, c->height, c->decode_surfaces, c->internal_profile, rt_format, render_fd); } if (s->pending || (s->encode_task && s->encode_task->pending)) synchronize(d, s); check(!s->derived_images, "surface has derived images", VA_STATUS_ERROR_SURFACE_BUSY); if (c->entrypoint == VAEntrypointEncSlice) { synchronize(d, s); check(bool(s->memory), "encode input has no image data", VA_STATUS_ERROR_INVALID_SURFACE); c->target = s; c->encode_picture = EncodePicture{}; return VA_STATUS_SUCCESS; } if (c->entrypoint == VAEntrypointVideoProc) { check(!s->acquired_handles && !s->mapped_images, "VPP target is in use", VA_STATUS_ERROR_SURFACE_BUSY); s->status = VA_STATUS_SUCCESS; c->target = s; c->vpp_pictures.clear(); c->vpp_failed = false; return VA_STATUS_SUCCESS; } if (!s->persistent_export) s->memory.reset(); s->status = VA_STATUS_SUCCESS; s->decoder = c->decoder; c->target = s; c->picture = Picture{}; c->hevc_picture = HevcPicture{}; c->vp9_picture = Vp9Picture{}; c->av1_picture = Av1Picture{}; return VA_STATUS_SUCCESS;) template static void render_buffer(P &p, const std::shared_ptr &b) { using Parameter = typename std::decay_t::value_type; using SliceType = typename std::decay_t::value_type; if (b->type == VAPictureParameterBufferType) { check(b->element_size >= sizeof(p.params) && b->elements == 1, "invalid picture parameters", VA_STATUS_ERROR_INVALID_BUFFER); std::memcpy(&p.params, b->data.data(), sizeof(p.params)); p.has_params = true; } else if (b->type == VAIQMatrixBufferType) { if constexpr (std::is_same_v || std::is_same_v) { throw Error(VA_STATUS_ERROR_UNSUPPORTED_BUFFERTYPE, "codec has no IQ matrix buffer"); } else { check(b->element_size >= sizeof(p.iq) && b->elements == 1, "invalid IQ matrix", VA_STATUS_ERROR_INVALID_BUFFER); std::memcpy(&p.iq, b->data.data(), sizeof(p.iq)); p.has_iq = true; } } else if (b->type == VASliceParameterBufferType) { check(b->element_size >= sizeof(Parameter), "invalid slice parameters", VA_STATUS_ERROR_INVALID_BUFFER); for (unsigned j = 0; j < b->elements; ++j) { Parameter s; std::memcpy(&s, b->data.data() + size_t(j) * b->element_size, sizeof(s)); p.pending_slices.push_back(s); } } else if (b->type == VASliceDataBufferType) { check(!p.pending_slices.empty(), "slice data missing parameters", VA_STATUS_ERROR_INVALID_BUFFER); for (const auto &s : p.pending_slices) { check(s.slice_data_flag == VA_SLICE_DATA_FLAG_ALL, "fragmented slice not implemented", VA_STATUS_ERROR_UNIMPLEMENTED); size_t off = s.slice_data_offset, n = s.slice_data_size; check(off <= b->data.size() && n <= b->data.size() - off, "slice data range", VA_STATUS_ERROR_INVALID_BUFFER); SliceType slice; slice.params = s; slice.bytes.assign(b->data.begin() + off, b->data.begin() + off + n); if (!std::is_same_v && !std::is_same_v && slice.bytes.size() >= 4 && !slice.bytes[0] && !slice.bytes[1]) { unsigned prefix = slice.bytes[2] == 1 ? 3 : !slice.bytes[2] && slice.bytes[3] == 1 ? 4 : 0; slice.bytes.erase(slice.bytes.begin(), slice.bytes.begin() + prefix); } p.slices.push_back(std::move(slice)); } p.pending_slices.clear(); } } static std::shared_ptr processing_filter(Driver &driver, VAContextID id, const VABufferID *filters, unsigned count) { check(count <= 1, "VPP supports one HDR filter", VA_STATUS_ERROR_UNSUPPORTED_FILTER); if (!count) return {}; check(filters, "null VPP filter list", VA_STATUS_ERROR_INVALID_PARAMETER); auto filter = buffer(driver, filters[0]); check(filter->context_id == id, "VPP filter belongs to another context", VA_STATUS_ERROR_INVALID_BUFFER); vpp_filter(*filter); check(context(driver, id)->vpp->tone_mapping_supported(), "HDR tone mapping unavailable", VA_STATUS_ERROR_UNSUPPORTED_FILTER); return filter; } API( render_picture, (VADriverContextP ctx, VAContextID id, VABufferID *ids, int count), auto c = context(d, id); check(bool(c->target), "no open picture"); try { check(count >= 0 && (!count || ids), "invalid picture buffer list", VA_STATUS_ERROR_INVALID_PARAMETER); if (c->entrypoint == VAEntrypointVideoProc) check(!c->vpp_failed, "invalid VPP submission", VA_STATUS_ERROR_INVALID_PARAMETER); std::shared_ptr deferred_av1_data; if (c->entrypoint == VAEntrypointVLD && is_av1(c->profile) && c->av1_picture.pending_slices.empty()) { // Chromium submits one AV1 data buffer before its tile parameters. // Consume it last, but preserve parameter/data pairing when a call // contains multiple data buffers (each has its own slice offsets). for (int i = 0; i < count; ++i) { auto b = buffer(d, ids[i]); if (b->type == VASliceParameterBufferType && !deferred_av1_data) break; if (b->type != VASliceDataBufferType) continue; if (deferred_av1_data) { deferred_av1_data.reset(); break; } deferred_av1_data = b; } } for (int i = 0; i < count; ++i) { auto b = buffer(d, ids[i]); if (b == deferred_av1_data) continue; if (c->entrypoint == VAEntrypointVideoProc) { check(b->context_id == id && b->type == VAProcPipelineParameterBufferType && b->element_size >= sizeof(VAProcPipelineParameterBuffer) && b->elements == 1, "invalid VPP pipeline buffer", VA_STATUS_ERROR_INVALID_BUFFER); check(c->vpp_pictures.size() < 16, "VPP supports at most 16 layers", VA_STATUS_ERROR_UNIMPLEMENTED); VAProcPipelineParameterBuffer params{}; std::memcpy(¶ms, b->data.data(), sizeof(params)); auto source = surface(d, params.surface); auto filter = processing_filter(d, id, params.filters, params.num_filters); c->vpp_pictures.push_back( vpp_picture(*b, source, *c->target, filter.get(), !c->vpp_pictures.empty())); } else if (c->entrypoint == VAEntrypointEncSlice) { check(b->context_id == id, "encode buffer belongs to another context", VA_STATUS_ERROR_INVALID_BUFFER); render_encode_buffer(c->encode_settings, c->encode_picture, *b); } else if (is_av1(c->profile)) render_buffer(c->av1_picture, b); else if (is_vp9(c->profile)) render_buffer(c->vp9_picture, b); else if (is_hevc(c->profile)) render_buffer(c->hevc_picture, b); else render_buffer(c->picture, b); } if (deferred_av1_data) render_buffer(c->av1_picture, deferred_av1_data); } catch (...) { if (c->entrypoint == VAEntrypointVideoProc) { c->vpp_pictures.clear(); c->vpp_failed = true; } throw; } return VA_STATUS_SUCCESS;) API( end_picture, (VADriverContextP ctx, VAContextID id), auto c = context(d, id); check(bool(c->target), "no open picture"); auto target = c->target; c->target.reset(); if (c->entrypoint == VAEntrypointVideoProc) { auto pictures = std::move(c->vpp_pictures); c->vpp_pictures.clear(); check(!c->vpp_failed && !pictures.empty(), "VPP pipeline parameters missing or rejected", VA_STATUS_ERROR_INVALID_PARAMETER); for (const auto &picture : pictures) { synchronize(d, picture.source); check(!picture.source->mapped_images && !picture.source->acquired_handles, "VPP source is in use", VA_STATUS_ERROR_SURFACE_BUSY); } synchronize(d, target); check(!target->derived_images && !target->acquired_handles, "VPP surface is in use", VA_STATUS_ERROR_SURFACE_BUSY); try { c->vpp->process(pictures, target); } catch (const Error &e) { target->status = e.status; throw; } return VA_STATUS_SUCCESS; } if (c->entrypoint == VAEntrypointEncSlice) { check(c->encode_picture.has_params, "missing encode picture", VA_STATUS_ERROR_INVALID_PARAMETER); auto coded = buffer(d, c->encode_picture.coded_buffer(c->encode_settings.hevc)); check(coded->type == VAEncCodedBufferType && coded->context_id == id, "invalid encode output buffer", VA_STATUS_ERROR_INVALID_BUFFER); auto reconstruction = surface(d, c->encode_picture.reconstruction(c->encode_settings.hevc)); synchronize_encode(reconstruction->encode_task); if (!c->encoder) { check(ctx->drm_state, "encoding requires a DRM display"); c->encoder = std::make_shared( d.encoder_device, c->encode_settings.hevc ? std::min(c->width, target->width) : c->width, c->encode_settings.hevc ? std::min(c->height, target->height) : c->height, c->profile, c->encode_settings, c->encode_picture, static_cast(ctx->drm_state)->fd); } auto task = c->encoder->encode(c->encode_settings, c->encode_picture, target, coded); task->encoder = c->encoder; target->encode_task = task; reconstruction->encode_task = task; return VA_STATUS_SUCCESS; } try { // Do not cache parameter sets from a rejected picture: the firmware // has not received them. Commit only after successful submission. if (is_av1(c->profile)) { check(c->av1_picture.has_params, "AV1 picture parameters missing", VA_STATUS_ERROR_INVALID_BUFFER); check(surface(d, c->av1_picture.params.current_frame) == target, "AV1 current frame does not match target", VA_STATUS_ERROR_INVALID_SURFACE); unsigned fourcc = c->av1_picture.params.bit_depth_idx == 1 ? VA_FOURCC_P010 : VA_FOURCC_NV12; check(fourcc == target->fourcc, "AV1 bit depth does not match target", VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT); auto next_state = c->av1; auto bytes = av1_bitstream(c->av1_picture, next_state, c->width, c->height); c->decoder->submit(bytes, target); c->av1 = std::move(next_state); } else if (is_vp9(c->profile)) { auto bytes = vp9_bitstream(c->profile, c->vp9_picture); c->decoder->submit(bytes, target); } else if (is_hevc(c->profile)) { auto next_state = c->hevc; auto bytes = hevc_bitstream(c->internal_profile, c->hevc_picture, next_state); c->decoder->submit(bytes, target); c->hevc = std::move(next_state); } else { auto next_state = c->h264; auto bytes = h264_bitstream(c->profile, c->picture, next_state); c->decoder->submit(bytes, target); c->h264 = std::move(next_state); } // Chromium's cached NativePixmap output does not call vaSyncSurface // after submission. Complete decode and copy before publishing success; // in particular, no completion fence exists for the CPU override. if (target->persistent_export) synchronize(d, target); } catch (const Error &e) { target->status = e.status; throw; } catch (...) { target->status = VA_STATUS_ERROR_DECODING_ERROR; throw; } return VA_STATUS_SUCCESS;) API(sync_surface, (VADriverContextP ctx, VASurfaceID id), synchronize(d, surface(d, id)); return VA_STATUS_SUCCESS;) API(query_status, (VADriverContextP ctx, VASurfaceID id, VASurfaceStatus *status), check(status, "null surface status", VA_STATUS_ERROR_INVALID_PARAMETER); auto s = surface(d, id); if (s->pending) if (auto decoder = s->decoder.lock()) decoder->refresh(); if (s->encode_task && s->encode_task->pending) if (auto encoder = s->encode_task->encoder.lock()) encoder->refresh(); *status = s->pending || (s->encode_task && s->encode_task->pending) ? VASurfaceRendering : VASurfaceReady; return VA_STATUS_SUCCESS;) API(query_error, (VADriverContextP ctx, VASurfaceID id, VAStatus status, void **info), surface(d, id); (void)status; *info = nullptr; return VA_STATUS_SUCCESS;) API(query_images, (VADriverContextP ctx, VAImageFormat *out, int *count), (void)d; out[0] = image_format(VA_FOURCC_NV12); out[1] = image_format(VA_FOURCC_I420); out[2] = image_format(VA_FOURCC_P010); out[3] = image_format(VA_FOURCC_BGRA); out[4] = image_format(VA_FOURCC_RGBA); *count = 5; return VA_STATUS_SUCCESS;) API(new_image, (VADriverContextP ctx, VAImageFormat *f, int w, int h, VAImage *out), *out = create_image(d, f->fourcc, w, h); return VA_STATUS_SUCCESS;) API(destroy_image, (VADriverContextP ctx, VAImageID id), auto image = lookup(d.images, id, VA_STATUS_ERROR_INVALID_IMAGE); check(buffer(d, image.buf)->acquired_fd < 0, "image handle acquired", VA_STATUS_ERROR_SURFACE_BUSY); d.buffers.erase(image.buf); d.images.erase(id); return VA_STATUS_SUCCESS;) API( derive_image, (VADriverContextP ctx, VASurfaceID id, VAImage *image), check(image, "null derived image", VA_STATUS_ERROR_INVALID_PARAMETER); auto s = surface(d, id); synchronize(d, s); check(!s->acquired_handles, "surface handle acquired", VA_STATUS_ERROR_SURFACE_BUSY); if (!s->memory) s->memory = allocate_undecoded_surface(ctx, *s); auto m = s->memory; VAImage result{}; result.image_id = d.next_id++; result.buf = d.next_id++; result.format = image_format(m->fourcc); result.width = s->width; result.height = s->height; result.data_size = m->size; result.num_planes = is_rgb(m->fourcc) ? 1 : 2; result.pitches[0] = result.pitches[1] = m->stride; result.offsets[0] = m->data_offset; result.offsets[1] = m->data_offset + m->stride * m->storage_height; if (is_rgb(m->fourcc)) { result.pitches[1] = 0; result.offsets[1] = 0; } auto b = std::make_shared(); b->type = VAImageBufferType; b->element_size = result.data_size; b->elements = 1; b->parent = s; b->memory = m; ++s->derived_images; d.buffers[result.buf] = b; try { d.images[result.image_id] = result; } catch (...) { d.buffers.erase(result.buf); throw; } *image = result; return VA_STATUS_SUCCESS;) API( put_image, (VADriverContextP ctx, VASurfaceID id, VAImageID image_id, int sx, int sy, unsigned sw, unsigned sh, int dx, int dy, unsigned dw, unsigned dh), auto s = surface(d, id); synchronize(d, s); auto image = lookup(d.images, image_id, VA_STATUS_ERROR_INVALID_IMAGE); check(sw == dw && sh == dh, "image scaling unsupported", VA_STATUS_ERROR_UNIMPLEMENTED); check(sw && sh && sx >= 0 && sy >= 0 && dx >= 0 && dy >= 0 && (is_rgb(s->fourcc) || !((sx | sy | dx | dy | sw | sh) & 1)) && uint64_t(sx) + sw <= image.width && uint64_t(sy) + sh <= image.height && uint64_t(dx) + dw <= s->width && uint64_t(dy) + dh <= s->height, "invalid upload region", VA_STATUS_ERROR_INVALID_PARAMETER); check(!s->derived_images && !s->acquired_handles, "surface has derived images or handles", VA_STATUS_ERROR_SURFACE_BUSY); check((is_rgb(s->fourcc) || is_rgb(image.format.fourcc)) ? s->fourcc == image.format.fourcc : (s->fourcc == VA_FOURCC_P010) == (image.format.fourcc == VA_FOURCC_P010), "image bit depth conversion unsupported", VA_STATUS_ERROR_INVALID_IMAGE_FORMAT); if (!s->memory) s->memory = allocate_undecoded_surface(ctx, *s); auto m = s->memory; auto source_buffer = buffer(d, image.buf); ImageMapping source(source_buffer); auto destination_buffer = std::make_shared(); destination_buffer->parent = s; destination_buffer->memory = m; ++s->derived_images; ImageMapping destination(destination_buffer); unsigned bytes = is_rgb(s->fourcc) ? 4 : s->fourcc == VA_FOURCC_P010 ? 2 : 1; for (unsigned row = 0; row < sh; ++row) std::memcpy(destination.data + m->data_offset + (dy + row) * m->stride + dx * bytes, source.data + image.offsets[0] + (sy + row) * image.pitches[0] + sx * bytes, sw *bytes); for (unsigned row = 0; !is_rgb(s->fourcc) && row < sh / 2; ++row) { auto uv = destination.data + m->data_offset + (m->storage_height + dy / 2 + row) * m->stride + dx * bytes; if (image.format.fourcc == VA_FOURCC_I420) { for (unsigned col = 0; col < sw / 2; ++col) { uv[2 * col] = source .data[image.offsets[1] + (sy / 2 + row) * image.pitches[1] + sx / 2 + col]; uv[2 * col + 1] = source .data[image.offsets[2] + (sy / 2 + row) * image.pitches[2] + sx / 2 + col]; } } else { std::memcpy( uv, source.data + image.offsets[1] + (sy / 2 + row) * image.pitches[1] + sx * bytes, sw * bytes); } } destination.finish(); source.finish(); return VA_STATUS_SUCCESS;) API(acquire_buffer_handle, (VADriverContextP ctx, VABufferID id, VABufferInfo *info), check(info, "null buffer handle info", VA_STATUS_ERROR_INVALID_PARAMETER); auto b = buffer(d, id); check(bool(b->memory), "only derived image buffers have DMA-BUF handles", VA_STATUS_ERROR_UNSUPPORTED_BUFFERTYPE); check(!info->mem_type || (info->mem_type & VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME), "buffer handle requires DRM PRIME", VA_STATUS_ERROR_UNSUPPORTED_MEMORY_TYPE); synchronize(d, b->parent); check(!b->parent->acquired_handles && !b->parent->mapped_images, "image is mapped or acquired", VA_STATUS_ERROR_SURFACE_BUSY); b->acquired_fd = fcntl(b->memory->fd, F_DUPFD_CLOEXEC, 0); check(b->acquired_fd >= 0, "duplicate image DMA-BUF fd"); ++b->parent->acquired_handles; if (!b->parent->token) b->parent->persistent_export = true; *info = {}; info->handle = b->acquired_fd; info->type = b->type; info->mem_type = VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME; info->mem_size = b->memory->size; return VA_STATUS_SUCCESS;) API( release_buffer_handle, (VADriverContextP ctx, VABufferID id), auto b = buffer(d, id); if (b->acquired_fd >= 0) { close(b->acquired_fd); b->acquired_fd = -1; --b->parent->acquired_handles; } return VA_STATUS_SUCCESS;) API( get_image, (VADriverContextP ctx, VASurfaceID id, int x, int y, unsigned w, unsigned h, VAImageID image_id), auto s = surface(d, id); synchronize(d, s); check(!s->derived_images, "surface has derived images", VA_STATUS_ERROR_SURFACE_BUSY); auto image = lookup(d.images, image_id, VA_STATUS_ERROR_INVALID_IMAGE); check(x >= 0 && y >= 0 && (is_rgb(s->fourcc) || (!(x & 1) && !(y & 1))) && w <= image.width && h <= image.height && uint64_t(x) + w <= s->width && uint64_t(y) + h <= s->height, "invalid image region", VA_STATUS_ERROR_INVALID_PARAMETER); auto out = buffer(d, image.buf); ImageMapping destination(out); if (!s->memory) { std::memset(destination.data, 0, image.data_size); destination.finish(); return VA_STATUS_SUCCESS; } auto m = s->memory; check((is_rgb(m->fourcc) || is_rgb(image.format.fourcc)) ? m->fourcc == image.format.fourcc : (m->fourcc == VA_FOURCC_P010) == (image.format.fourcc == VA_FOURCC_P010), "image bit depth conversion unsupported", VA_STATUS_ERROR_INVALID_IMAGE_FORMAT); unsigned bytes_per_sample = is_rgb(m->fourcc) ? 4 : m->fourcc == VA_FOURCC_P010 ? 2 : 1; check(uint64_t(x) + w <= m->width && uint64_t(y) + h <= m->height, "image exceeds CAPTURE layout"); uint8_t *base = m->map() + m->data_offset; dma_buf_sync sync{DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ}; check(ioctl(m->fd, DMA_BUF_IOCTL_SYNC, &sync) == 0, "DMA-BUF read synchronization failed"); for (unsigned row = 0; row < h; ++row) std::memcpy(destination.data + image.offsets[0] + row * image.pitches[0], base + (y + row) * m->stride + x * bytes_per_sample, w *bytes_per_sample); uint8_t *uv = base + m->stride * m->storage_height + (y / 2) * m->stride + x * bytes_per_sample; for (unsigned row = 0; !is_rgb(m->fourcc) && row < (h + 1) / 2; ++row) { if (image.format.fourcc != VA_FOURCC_I420) std::memcpy(destination.data + image.offsets[1] + row * image.pitches[1], uv + row * m->stride, ((w + 1) & ~1u) * bytes_per_sample); else for (unsigned col = 0; col < (w + 1) / 2; ++col) { destination.data[image.offsets[1] + row * image.pitches[1] + col] = uv[row * m->stride + 2 * col]; destination.data[image.offsets[2] + row * image.pitches[2] + col] = uv[row * m->stride + 2 * col + 1]; } } sync.flags = DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ; check(ioctl(m->fd, DMA_BUF_IOCTL_SYNC, &sync) == 0, "DMA-BUF read synchronization end failed"); destination.finish(); return VA_STATUS_SUCCESS;) API( export_surface, (VADriverContextP ctx, VASurfaceID id, uint32_t type, uint32_t flags, void *out), check(out, "null surface descriptor", VA_STATUS_ERROR_INVALID_PARAMETER); check(type == VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, "export requires DRM PRIME2", VA_STATUS_ERROR_UNSUPPORTED_MEMORY_TYPE); auto s = surface(d, id); synchronize(d, s); // GPU producers such as Sunshine render into exported encoder input. // Independent storage is writable: Iris CAPTURE allocations // may still be decoder references and have cached CPU aliases. check(!(flags & VA_EXPORT_SURFACE_WRITE_ONLY) || !s->memory || s->memory->origin == MemoryOrigin::MsmCoherent || s->memory->origin == MemoryOrigin::MsmWriteCombined || s->memory->origin == MemoryOrigin::DmaHeap || s->memory->origin == MemoryOrigin::Imported, "writable export requires independent storage", VA_STATUS_ERROR_UNIMPLEMENTED); if (!s->memory) s->memory = allocate_undecoded_surface(ctx, *s); auto m = s->memory; auto &desc = *static_cast(out); desc = {}; desc.fourcc = m->fourcc; desc.width = s->width; desc.height = s->height; desc.num_objects = 1; desc.objects[0].fd = fcntl(m->fd, F_DUPFD_CLOEXEC, 0); check(desc.objects[0].fd >= 0, "duplicate DMA-BUF fd"); if (!s->token) { if (!s->persistent_export) trace("surface=%#x persistent GEM export (copy if later decoded)", id); s->persistent_export = true; } desc.objects[0] .size = m->size; desc.objects[0].drm_format_modifier = DRM_FORMAT_MOD_LINEAR; if (is_rgb(m->fourcc)) { desc.num_layers = 1; desc.layers[0].drm_format = m->fourcc == VA_FOURCC_BGRA ? DRM_FORMAT_ARGB8888 : DRM_FORMAT_ABGR8888; desc.layers[0].num_planes = 1; desc.layers[0].pitch[0] = m->stride; desc.layers[0].offset[0] = m->data_offset; } else if (!(flags & VA_EXPORT_SURFACE_COMPOSED_LAYERS)) { desc.num_layers = 2; desc.layers[0].drm_format = m->fourcc == VA_FOURCC_P010 ? DRM_FORMAT_R16 : DRM_FORMAT_R8; desc.layers[1].drm_format = m->fourcc == VA_FOURCC_P010 ? DRM_FORMAT_GR1616 : DRM_FORMAT_GR88; for (unsigned i = 0; i < 2; ++i) { desc.layers[i].num_planes = 1; desc.layers[i].pitch[0] = m->stride; desc.layers[i].offset[0] = m->data_offset + (i ? m->stride * m->storage_height : 0); } } else { desc.num_layers = 1; desc.layers[0].drm_format = m->fourcc == VA_FOURCC_P010 ? DRM_FORMAT_P010 : DRM_FORMAT_NV12; desc.layers[0].num_planes = 2; desc.layers[0].pitch[0] = desc.layers[0].pitch[1] = m->stride; desc.layers[0].offset[0] = m->data_offset; desc.layers[0].offset[1] = m->data_offset + m->stride * m->storage_height; } return VA_STATUS_SUCCESS;) API( query_vpp_filters, (VADriverContextP ctx, VAContextID id, VAProcFilterType *filters, unsigned *count), check(context(d, id)->entrypoint == VAEntrypointVideoProc, "not a VPP context", VA_STATUS_ERROR_INVALID_CONTEXT); check(count, "null filter count", VA_STATUS_ERROR_INVALID_PARAMETER); unsigned capacity = *count; *count = context(d, id)->vpp->tone_mapping_supported() ? 1 : 0; check(capacity >= *count, "filter list too small", VA_STATUS_ERROR_MAX_NUM_EXCEEDED); if (*count) { check(filters, "null filter list", VA_STATUS_ERROR_INVALID_PARAMETER); filters[0] = VAProcFilterHighDynamicRangeToneMapping; } return VA_STATUS_SUCCESS;) API(query_vpp_filter_caps, (VADriverContextP ctx, VAContextID id, VAProcFilterType type, void *caps, unsigned *count), check(context(d, id)->entrypoint == VAEntrypointVideoProc, "not a VPP context", VA_STATUS_ERROR_INVALID_CONTEXT); check(count, "null filter caps count", VA_STATUS_ERROR_INVALID_PARAMETER); unsigned capacity = *count; *count = 0; check(type == VAProcFilterHighDynamicRangeToneMapping && context(d, id)->vpp->tone_mapping_supported(), "unsupported VPP filter", VA_STATUS_ERROR_UNSUPPORTED_FILTER); *count = 1; check(capacity, "filter caps list too small", VA_STATUS_ERROR_MAX_NUM_EXCEEDED); check(caps, "null filter caps", VA_STATUS_ERROR_INVALID_PARAMETER); VAProcFilterCapHighDynamicRange hdr{}; hdr.metadata_type = VAProcHighDynamicRangeMetadataHDR10; hdr.caps_flag = VA_TONE_MAPPING_HDR_TO_SDR; std::memcpy(caps, &hdr, sizeof(hdr)); return VA_STATUS_SUCCESS;) API(query_vpp_caps, (VADriverContextP ctx, VAContextID id, VABufferID *filters, unsigned count, VAProcPipelineCaps *caps), check(context(d, id)->entrypoint == VAEntrypointVideoProc, "not a VPP context", VA_STATUS_ERROR_INVALID_CONTEXT); check(caps, "null pipeline caps", VA_STATUS_ERROR_INVALID_PARAMETER); auto filter = processing_filter(d, id, filters, count); vpp_caps(*caps, bool(filter)); return VA_STATUS_SUCCESS;) static VAStatus terminate(VADriverContextP ctx) { delete static_cast(ctx->pDriverData); ctx->pDriverData = nullptr; return VA_STATUS_SUCCESS; } } // namespace irisva extern "C" VAStatus __vaDriverInit_1_0(VADriverContextP ctx) { using namespace irisva; try { auto d = std::make_unique(); d->device = find_device(d->av1); d->encoder_device = find_encoder_device(); ctx->version_major = VA_MAJOR_VERSION; ctx->version_minor = VA_MINOR_VERSION; ctx->max_profiles = 9; ctx->max_entrypoints = 2; ctx->max_attributes = 16; // libva validates nonzero allocation bounds even when queries return no entries. ctx->max_image_formats = 5; ctx->max_subpic_formats = 1; ctx->max_display_attributes = 1; ctx->str_vendor = "Iris V4L2 stateful VA-API backend"; auto &v = *ctx->vtable; if (ctx->vtable_vpp && vpp_supported(VAProfileNone, VAEntrypointVideoProc)) { auto &vpp = *ctx->vtable_vpp; vpp.version = VA_DRIVER_VTABLE_VPP_VERSION; vpp.vaQueryVideoProcFilters = query_vpp_filters; vpp.vaQueryVideoProcFilterCaps = query_vpp_filter_caps; vpp.vaQueryVideoProcPipelineCaps = query_vpp_caps; } v.vaTerminate = terminate; v.vaQueryConfigProfiles = query_profiles; v.vaQueryConfigEntrypoints = query_entrypoints; v.vaGetConfigAttributes = get_attributes; v.vaCreateConfig = create_config; v.vaDestroyConfig = destroy_config; v.vaQueryConfigAttributes = query_config; v.vaQuerySurfaceAttributes = query_surfaces; v.vaCreateSurfaces2 = create_surfaces; v.vaCreateSurfaces = create_surfaces_legacy; v.vaDestroySurfaces = destroy_surfaces; v.vaCreateContext = create_context; v.vaDestroyContext = destroy_context; v.vaCreateBuffer = create_buffer; v.vaBufferSetNumElements = set_elements; v.vaMapBuffer = map_buffer; v.vaUnmapBuffer = unmap_buffer; v.vaDestroyBuffer = destroy_buffer; v.vaBufferInfo = buffer_info; v.vaBeginPicture = begin_picture; v.vaRenderPicture = render_picture; v.vaEndPicture = end_picture; v.vaSyncSurface = sync_surface; v.vaSyncBuffer = sync_buffer; v.vaQuerySurfaceStatus = query_status; v.vaQuerySurfaceError = query_error; v.vaQueryImageFormats = query_images; v.vaCreateImage = new_image; v.vaDestroyImage = destroy_image; v.vaDeriveImage = derive_image; v.vaGetImage = get_image; v.vaExportSurfaceHandle = export_surface; v.vaAcquireBufferHandle = acquire_buffer_handle; v.vaReleaseBufferHandle = release_buffer_handle; v.vaPutImage = put_image; v.vaPutSurface = [](VADriverContextP, VASurfaceID, void *, short, short, unsigned short, unsigned short, short, short, unsigned short, unsigned short, VARectangle *, unsigned, unsigned) { return VA_STATUS_ERROR_UNIMPLEMENTED; }; v.vaQuerySubpictureFormats = [](VADriverContextP, VAImageFormat *, unsigned *, unsigned *n) { *n = 0; return VA_STATUS_SUCCESS; }; v.vaQueryDisplayAttributes = [](VADriverContextP, VADisplayAttribute *, int *n) { *n = 0; return VA_STATUS_SUCCESS; }; v.vaGetDisplayAttributes = [](VADriverContextP, VADisplayAttribute *, int) { return VA_STATUS_ERROR_ATTR_NOT_SUPPORTED; }; v.vaSetDisplayAttributes = [](VADriverContextP, VADisplayAttribute *, int) { return VA_STATUS_ERROR_ATTR_NOT_SUPPORTED; }; v.vaSetImagePalette = [](VADriverContextP, VAImageID, unsigned char *) { return VA_STATUS_ERROR_UNIMPLEMENTED; }; v.vaCreateSubpicture = [](VADriverContextP, VAImageID, VASubpictureID *) { return VA_STATUS_ERROR_UNIMPLEMENTED; }; v.vaDestroySubpicture = [](VADriverContextP, VASubpictureID) { return VA_STATUS_ERROR_UNIMPLEMENTED; }; v.vaSetSubpictureImage = [](VADriverContextP, VASubpictureID, VAImageID) { return VA_STATUS_ERROR_UNIMPLEMENTED; }; v.vaSetSubpictureChromakey = [](VADriverContextP, VASubpictureID, unsigned, unsigned, unsigned) { return VA_STATUS_ERROR_UNIMPLEMENTED; }; v.vaSetSubpictureGlobalAlpha = [](VADriverContextP, VASubpictureID, float) { return VA_STATUS_ERROR_UNIMPLEMENTED; }; v.vaAssociateSubpicture = [](VADriverContextP, VASubpictureID, VASurfaceID *, int, short, short, unsigned short, unsigned short, short, short, unsigned short, unsigned short, unsigned) { return VA_STATUS_ERROR_UNIMPLEMENTED; }; v.vaDeassociateSubpicture = [](VADriverContextP, VASubpictureID, VASurfaceID *, int) { return VA_STATUS_ERROR_UNIMPLEMENTED; }; trace("initialized on %s", d->device.c_str()); ctx->pDriverData = d.release(); return VA_STATUS_SUCCESS; } catch (const std::exception &e) { std::fprintf(stderr, "iris-vaapi: init: %s\n", e.what()); return VA_STATUS_ERROR_OPERATION_FAILED; } } qcom-va-driver-1.1.0/src/cdsp/000077500000000000000000000000001526764642600161075ustar00rootroot00000000000000qcom-va-driver-1.1.0/src/cdsp/README.md000066400000000000000000000057661526764642600174040ustar00rootroot00000000000000# CDSP VPP NV12/P010 scaling, bit-depth conversion and 90°/180°/270° rotation using 128-byte HVX vectors. Scaling uses area reduction and Catmull–Rom enlargement, with dedicated 2× paths. See [supported inputs and usage](../../README.md). ## Build and runtime - Linux ARM64 build machine; GHC with `syb` (`ghc haskell-syb` on Arch, `ghc libghc-syb-dev` on Debian). No full Hexagon SDK required. - Meson downloads pinned QAIC and Hexagon Open Access **19.0.02** archives. The toolchain is about 855 MiB compressed and needs several GiB extracted. - FastRPC defaults to the system library, falling back to pinned source. The fallback needs libyaml/libbsd development files. Use `-Dfastrpc=system` or `bundled` to force either; `-Dfastrpc_include_dir=...` overrides headers. - Offline builds: prefill `subprojects/packagecache/` using filenames and hashes from `subprojects/*.wrap`, then pass `--wrap-mode=nodownload`. - Target: CDSP firmware/runtime libraries, unsigned FastRPC support, `/dev/fastrpc-cdsp` and board FastRPC configuration. Buffer allocation uses `/dev/dma_heap/system` with coherent MSM GEM as a fallback. Every build produces v68/v69/v73/v75/v79/v81 skeletons. The helper detects the architecture and loads the exact match; unsupported architectures fail. The install path is tried first unless `ADSP_LIBRARY_PATH` is set and nonempty. Default installation with `/usr` prefix: - Skeletons: `/usr/lib/dsp/cdsp/qcom-va-driver/vpp_v_skel.so` (`-Dcdsp_firmware_dir=...` to override). - Host helper and optional private FastRPC library: `libdir/qcom-va-driver`. For uninstalled builds, set `LD_LIBRARY_PATH` and `ADSP_LIBRARY_PATH` to `build/src/cdsp`; see [test commands](../../tests/README.md#cdsp-vpp). ## Maintenance - `dsp.c` / `filter.h`: Q7 area and Q14 cubic filters, cached per context. P010 operations retain signed Q4 intermediates; NV12 output rounds and saturates after filtering. Conversion preserves color range and matrix. Rotation transposes pixel groups (keeping U/V paired); 90°/270° HVX tiles write 128-byte spans using 32–128 rows. Strips and transpose workspace prefer VTCM, falling back to aligned memory when unavailable. Scratch is sized for active workers and released when no longer needed or reconfigured. Active workers follow the HVX count (1–8); each context currently allocates eight DSP threads. Multiple contexts therefore share a finite thread budget. Contexts request maximum core/bus performance. - `client.cpp`: persistent DMA-BUF mappings, retired before the last RPC context closes and remapped lazily if surfaces survive. Imports retain their FDs; submissions and mapping/session changes are synchronized. - `qurt_abi.h`: minimal firmware declarations, not a QuRT implementation. Layouts/constants are audited against SDK 6.7; keep the short-enum ABI. All six architectures build and pass the ABI audit; hardware tests cover v68. Run [scaling, mapping-lifetime and ABI checks](../../tests/README.md#cdsp-vpp) when changing these interfaces. qcom-va-driver-1.1.0/src/cdsp/build-dsp.py000066400000000000000000000040041526764642600203420ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT """Invoke the independent Hexagon compiler from a native Linux Meson build.""" from pathlib import Path import re import subprocess import sys arch = sys.argv[1] if arch not in ('v68', 'v69', 'v73', 'v75', 'v79', 'v81'): raise SystemExit('Unsupported DSP architecture: ' + arch) tools, rpc_include_dir, generated, source, output = map(Path, sys.argv[2:]) cc = tools / 'bin/hexagon-clang' resource = subprocess.check_output([str(cc), '--no-default-config', '-print-resource-dir'], text=True).strip() flags = ['--no-default-config', '--target=hexagon', '-m' + arch, '-mhvx=' + arch, '-mhvx-length=128B', '-O3', '-fshort-enums', '-G0', '-fPIC', '-fno-stack-protector', '-Wall', '-Werror', '-nostdinc', '-isystem', resource + '/include', '-isystem', str(tools / 'target/hexagon/include'), '-I' + str(rpc_include_dir), '-I' + str(generated), '-I' + str(source)] objects = [] object_dir = output.parent / arch object_dir.mkdir(exist_ok=True) # QAIC embeds its default module filename in an exported URI. Keep that # metadata consistent with this variant, including the string's array size. skel = object_dir / 'iris_vpp_skel.c' skel_text, count = re.subn(r'(char iris_vpp_skel_handle_invoke_uri)\[[^\]]*\]', r'\1[]', (generated / skel.name).read_text()) if count != 1 or skel_text.count('libiris_vpp_skel.so') != 1: raise SystemExit('Unexpected QAIC skeleton URI declaration') skel.write_text(skel_text.replace('libiris_vpp_skel.so', output.name)) for src in [skel, source / 'dsp.c']: obj = object_dir / (src.stem + '.o') subprocess.run([str(cc), *flags, '-c', str(src), '-o', str(obj)], check=True) objects.append(str(obj)) subprocess.run([str(cc), '--no-default-config', '-m' + arch, '-shared', '-G0', '-Wl,-Bsymbolic', '-Wl,-soname,' + output.name, *['-Wl,--wrap=' + name for name in ['malloc', 'calloc', 'free', 'realloc', 'memalign']], *objects, '-o', str(output)], check=True) qcom-va-driver-1.1.0/src/cdsp/client.cpp000066400000000000000000000265341526764642600201030ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "client.h" #include "iris_vpp.h" #include "skel.h" #include #include #include #include #include #include #include #include #include #include struct cdsp_vpp_context { remote_handle64 handle = 0; int workers; unsigned src_width = 0, src_height = 0, dst_width = 0, dst_height = 0; std::mutex lock; }; struct cdsp_vpp_buffer { int fd = -1; size_t size = 0; void *map = MAP_FAILED; dev_t dev; ino_t ino; bool dsp_mapped = false; cdsp_vpp_buffer *mapped_next = nullptr; }; // The FastRPC library can close its session when the last RPC handle closes. // Retire our persistent maps before that happens, even when imported buffers // outlive all contexts. A later context will map those buffers again lazily. static std::mutex mapping_lock; static cdsp_vpp_buffer *mapped_buffers = nullptr; static unsigned active_contexts = 0; extern "C" int cdsp_vpp_create(cdsp_vpp_context **out, int workers, int max_power) { if (!out || workers < 0 || workers > CDSP_VPP_MAX_WORKERS) return -EINVAL; *out = nullptr; auto *ctx = new (std::nothrow) cdsp_vpp_context; if (!ctx) return -ENOMEM; std::lock_guard mappings(mapping_lock); remote_rpc_control_unsigned_module unsigned_session{CDSP_DOMAIN_ID, 1}; int e = remote_session_control(DSPRPC_CONTROL_UNSIGNED_MODULE, &unsigned_session, sizeof(unsigned_session)); if (!e) e = cdsp_vpp_select_workers(&workers); if (!e) e = cdsp_vpp_open_skel(&ctx->handle); if (!e && max_power) { int mask = 0; e = iris_vpp_power(ctx->handle, &mask); if (!e && mask != 7) e = -EIO; } if (e) { if (ctx->handle) iris_vpp_close(ctx->handle); delete ctx; return e; } ctx->workers = workers; active_contexts++; *out = ctx; return 0; } extern "C" void cdsp_vpp_destroy(cdsp_vpp_context *ctx) { if (ctx) { std::lock_guard mappings(mapping_lock); if (active_contexts == 1) { while (mapped_buffers) { auto *buffer = mapped_buffers; fastrpc_munmap(CDSP_DOMAIN_ID, buffer->fd, buffer->map, buffer->size); buffer->dsp_mapped = false; mapped_buffers = buffer->mapped_next; buffer->mapped_next = nullptr; } } iris_vpp_close(ctx->handle); active_contexts--; delete ctx; } } extern "C" int cdsp_vpp_get_info(cdsp_vpp_context *ctx, uint32_t *hz, int *units) { if (!ctx || !hz || !units) return -EINVAL; std::lock_guard guard(ctx->lock); return iris_vpp_info(ctx->handle, hz, units); } extern "C" int cdsp_vpp_import(int fd, size_t size, cdsp_vpp_buffer **out) { if (!out || fd < 0 || !size || size > INT_MAX) return -EINVAL; *out = nullptr; struct stat st{}; if (fstat(fd, &st)) return -errno; if (st.st_size <= 0 || size > (uint64_t)st.st_size) return -EINVAL; auto *buffer = new (std::nothrow) cdsp_vpp_buffer; if (!buffer) return -ENOMEM; buffer->size = size; buffer->dev = st.st_dev; buffer->ino = st.st_ino; buffer->fd = fcntl(fd, F_DUPFD_CLOEXEC, 0); if (buffer->fd < 0) { int e = -errno; delete buffer; return e; } buffer->map = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, buffer->fd, 0); if (buffer->map == MAP_FAILED) { int e = -errno; close(buffer->fd); delete buffer; return e; } remote_register_buf(buffer->map, (int)size, buffer->fd); *out = buffer; return 0; } extern "C" void cdsp_vpp_release(cdsp_vpp_buffer *buffer) { if (buffer) { std::lock_guard mappings(mapping_lock); if (buffer->dsp_mapped) { fastrpc_munmap(CDSP_DOMAIN_ID, buffer->fd, buffer->map, buffer->size); auto **entry = &mapped_buffers; while (*entry != buffer) entry = &(*entry)->mapped_next; *entry = buffer->mapped_next; } remote_register_buf(buffer->map, (int)buffer->size, -1); munmap(buffer->map, buffer->size); close(buffer->fd); delete buffer; } } extern "C" void *cdsp_vpp_buffer_map(cdsp_vpp_buffer *buffer) { return buffer ? buffer->map : nullptr; } extern "C" int cdsp_vpp_buffer_fd(const cdsp_vpp_buffer *buffer) { return buffer ? buffer->fd : -1; } static bool valid_frame(const cdsp_vpp_frame &frame, int format) { if (format != CDSP_VPP_NV12 && format != CDSP_VPP_P010) return false; unsigned bytes = format == CDSP_VPP_P010 ? 2 : 1; if (!frame.buffer || frame.width < 2 || frame.width > 8192 || frame.height < 2 || frame.height > 8192 || (frame.width & 1) || (frame.height & 1) || frame.stride < frame.width * bytes || frame.stride > INT_MAX || frame.y_offset > INT_MAX || frame.uv_offset > INT_MAX || ((frame.stride | frame.y_offset | frame.uv_offset) & (bytes - 1))) return false; uint64_t y_end = frame.y_offset + (uint64_t)(frame.height - 1) * frame.stride + frame.width * bytes; uint64_t uv_end = frame.uv_offset + (uint64_t)(frame.height / 2 - 1) * frame.stride + frame.width * bytes; return y_end <= frame.buffer->size && uv_end <= frame.buffer->size && (y_end <= frame.uv_offset || uv_end <= frame.y_offset); } static int wait_dma_buf_access(int fd, short events) { pollfd descriptor{fd, events, 0}; int e; do { e = poll(&descriptor, 1, -1); } while (e < 0 && errno == EINTR); if (e < 0) return -errno; return descriptor.revents & (POLLERR | POLLNVAL | POLLHUP) ? -EIO : 0; } static int ensure_dsp_map(cdsp_vpp_buffer &buffer) { std::lock_guard guard(mapping_lock); if (buffer.dsp_mapped) return 0; // STATIC keeps the DMA/SMMU and DSP mappings alive across frames while // retaining normal FastRPC cache maintenance. Map lazily after context // creation has selected the unsigned CDSP session. int e = fastrpc_mmap(CDSP_DOMAIN_ID, buffer.fd, buffer.map, 0, buffer.size, FASTRPC_MAP_STATIC); if (!e) { buffer.dsp_mapped = true; buffer.mapped_next = mapped_buffers; mapped_buffers = &buffer; } return e; } extern "C" int cdsp_vpp_profile(cdsp_vpp_context *ctx, int enable, uint64_t counters[CDSP_VPP_PROFILE_COUNTERS]) { if (!ctx || !counters || (enable != 0 && enable != 1)) return -EINVAL; std::lock_guard guard(ctx->lock); return iris_vpp_profile(ctx->handle, enable, counters, CDSP_VPP_PROFILE_COUNTERS); } extern "C" int cdsp_vpp_scale(cdsp_vpp_context *ctx, const cdsp_vpp_frame *src, const cdsp_vpp_crop *crop, const cdsp_vpp_frame *dst, uint64_t *us) { return cdsp_vpp_process(ctx, src, CDSP_VPP_NV12, crop, dst, CDSP_VPP_NV12, us); } extern "C" int cdsp_vpp_process(cdsp_vpp_context *ctx, const cdsp_vpp_frame *src, int src_format, const cdsp_vpp_crop *crop, const cdsp_vpp_frame *dst, int dst_format, uint64_t *us) { return cdsp_vpp_transform(ctx, src, src_format, crop, dst, dst_format, CDSP_VPP_ROTATE_NONE, us); } extern "C" int cdsp_vpp_transform(cdsp_vpp_context *ctx, const cdsp_vpp_frame *src, int src_format, const cdsp_vpp_crop *crop, const cdsp_vpp_frame *dst, int dst_format, int rotation, uint64_t *us) { if (rotation < CDSP_VPP_ROTATE_NONE || rotation > CDSP_VPP_ROTATE_270) return -EINVAL; if (!ctx || !src || !dst || !us || !valid_frame(*src, src_format) || !valid_frame(*dst, dst_format)) return -EINVAL; auto &src_buffer = *src->buffer; auto &dst_buffer = *dst->buffer; if (src_buffer.dev == dst_buffer.dev && src_buffer.ino == dst_buffer.ino) return -EINVAL; unsigned scaled_width = (rotation & 1) ? dst->height : dst->width; unsigned scaled_height = (rotation & 1) ? dst->width : dst->height; cdsp_vpp_crop region = crop ? *crop : cdsp_vpp_crop{0, 0, src->width, src->height}; if (!region.width || !region.height || ((region.x | region.width) & 1) || ((region.y | region.height) & 1) || (uint64_t)region.x + region.width > src->width || (uint64_t)region.y + region.height > src->height || region.width > 20u * scaled_width || region.height > 20u * scaled_height || !((region.width >= scaled_width && region.height >= scaled_height) || (region.width <= scaled_width && region.height <= scaled_height))) return -EINVAL; std::lock_guard guard(ctx->lock); int e = wait_dma_buf_access(src_buffer.fd, POLLIN); if (!e) e = wait_dma_buf_access(dst_buffer.fd, POLLOUT); if (e) return e; e = ensure_dsp_map(src_buffer); if (!e) e = ensure_dsp_map(dst_buffer); if (e) return e; if (ctx->src_width != region.width || ctx->src_height != region.height || ctx->dst_width != scaled_width || ctx->dst_height != scaled_height) { e = iris_vpp_configure(ctx->handle, region.width, region.height, scaled_width, scaled_height); ctx->src_width = ctx->src_height = ctx->dst_width = ctx->dst_height = 0; if (e) return e; ctx->src_width = region.width; ctx->src_height = region.height; ctx->dst_width = scaled_width; ctx->dst_height = scaled_height; } unsigned src_bytes = src_format == CDSP_VPP_P010 ? 2 : 1; return iris_vpp_transform( ctx->handle, (const uint8_t *)src_buffer.map, (int)src_buffer.size, (uint8_t *)dst_buffer.map, (int)dst_buffer.size, src->stride, dst->stride, src->y_offset + (uint64_t)region.y * src->stride + region.x * src_bytes, src->uv_offset + (uint64_t)(region.y / 2) * src->stride + region.x * src_bytes, dst->y_offset, dst->uv_offset, ctx->workers, src_format, dst_format, rotation, us); } extern "C" int cdsp_vpp_overlay(cdsp_vpp_context *ctx, cdsp_vpp_buffer *base, cdsp_vpp_buffer *rgba, cdsp_vpp_buffer *dst, const OverlayParams *p, uint64_t *us) { if (!ctx || !base || !rgba || !dst || !p || !us || !overlay_valid(p, base->size, rgba->size, dst->size)) return -EINVAL; auto same = [](const cdsp_vpp_buffer *a, const cdsp_vpp_buffer *b) { return a->dev == b->dev && a->ino == b->ino; }; if (same(rgba, dst) || (same(base, dst) && (p->src_y != p->dst_y || p->src_uv != p->dst_uv || p->src_stride != p->dst_stride))) return -EINVAL; std::lock_guard guard(ctx->lock); for (auto buffer : {base, rgba, dst}) { int e = wait_dma_buf_access(buffer->fd, buffer == dst ? POLLOUT : POLLIN); if (!e) e = ensure_dsp_map(*buffer); if (e) return e; } return iris_vpp_overlay(ctx->handle, (const uint8_t *)base->map, base->size, (const uint8_t *)rgba->map, rgba->size, (uint8_t *)dst->map, dst->size, (const uint8_t *)p, sizeof(*p), ctx->workers, us); } qcom-va-driver-1.1.0/src/cdsp/client.h000066400000000000000000000066471526764642600175530ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef CDSP_VPP_CLIENT_H #define CDSP_VPP_CLIENT_H #include "vpp_limits.h" #include "../overlay-params.h" #include #include #ifdef __cplusplus extern "C" { #endif typedef struct cdsp_vpp_context cdsp_vpp_context; typedef struct cdsp_vpp_buffer cdsp_vpp_buffer; typedef struct { cdsp_vpp_buffer *buffer; unsigned width, height, stride; size_t y_offset, uv_offset; } cdsp_vpp_frame; typedef struct { unsigned x, y, width, height; } cdsp_vpp_crop; // 0 on success; negative errno for host validation/syscalls, positive FastRPC // error otherwise. One context serializes submissions. Caller must not destroy // a context/buffer while an operation is using it. Import duplicates the fd. // DMA/SMMU and DSP mappings are retained until buffer release or destruction of // the last context. Buffers can outlive contexts and are remapped on later use. // workers: 0 = scalar diagnostic; 1..CDSP_VPP_MAX_WORKERS = maximum HVX workers. // Actual concurrency is capped by HVX_SUPPORT_128B. Pass CDSP_VPP_MAX_WORKERS to use the // largest supported count automatically. HVX query failure or zero 128-byte // contexts fails creation. max_power enables a per-context // core/bus MAX vote, released on destroy. Firmware can still thermally // throttle. int cdsp_vpp_create(cdsp_vpp_context **out, int workers, int max_power); void cdsp_vpp_destroy(cdsp_vpp_context *ctx); int cdsp_vpp_get_info(cdsp_vpp_context *ctx, uint32_t *hz, int *hvx_units); // Diagnostic only: per-row timestamps perturb timings. Returns the last frame's // microseconds, four entries per worker: Y vertical/horizontal, UV vertical/horizontal. int cdsp_vpp_profile(cdsp_vpp_context *ctx, int enable, uint64_t counters[CDSP_VPP_PROFILE_COUNTERS]); int cdsp_vpp_import(int fd, size_t size, cdsp_vpp_buffer **out); void cdsp_vpp_release(cdsp_vpp_buffer *buffer); // The map is provided for diagnostic CPU access only; pair access with // DMA_BUF_IOCTL_SYNC. Processing performs no CPU access to frame pixels. void *cdsp_vpp_buffer_map(cdsp_vpp_buffer *buffer); int cdsp_vpp_buffer_fd(const cdsp_vpp_buffer *buffer); // borrowed fd int cdsp_vpp_scale(cdsp_vpp_context *ctx, const cdsp_vpp_frame *src, const cdsp_vpp_crop *crop, const cdsp_vpp_frame *dst, uint64_t *dsp_usec); // Formats are CDSP_VPP_NV12/P010; strides and offsets are always in bytes. // NV12 -> P010 shifts each byte left by 8. P010 -> NV12 rounds and saturates; // color range and matrix are unchanged. cdsp_vpp_scale is the NV12-only wrapper. int cdsp_vpp_process(cdsp_vpp_context *ctx, const cdsp_vpp_frame *src, int src_format, const cdsp_vpp_crop *crop, const cdsp_vpp_frame *dst, int dst_format, uint64_t *dsp_usec); // Base and destination may be the same allocation only with identical layouts. int cdsp_vpp_overlay(cdsp_vpp_context *ctx, cdsp_vpp_buffer *base, cdsp_vpp_buffer *rgba, cdsp_vpp_buffer *dst, const OverlayParams *params, uint64_t *dsp_usec); // Clockwise CDSP_VPP_ROTATE_* rotation. Destination dimensions describe the // final rotated image; cropping uses source coordinates. All geometry is even. int cdsp_vpp_transform(cdsp_vpp_context *ctx, const cdsp_vpp_frame *src, int src_format, const cdsp_vpp_crop *crop, const cdsp_vpp_frame *dst, int dst_format, int rotation, uint64_t *dsp_usec); #ifdef __cplusplus } #endif #endif qcom-va-driver-1.1.0/src/cdsp/dsp.c000066400000000000000000002314071526764642600170500ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "filter.h" #include "../overlay-params.h" #include "iris_vpp.h" #include "qurt_abi.h" #include "vpp_limits.h" #include #include #include #include #include #include // Horizontal gathers address at most two 128-byte input vectors. indices are // byte offsets relative to src_base; dst_offset/output_bytes describe output. typedef struct AreaTile { uint8_t indices[FILTER_MAX_TAPS][128]; int8_t weights[FILTER_MAX_TAPS][128]; // Q7 bits, read as unsigned for area reduction. uint8_t delta_controls[FILTER_MAX_TAPS][2][128]; int use_delta[FILTER_MAX_TAPS]; int src_base, dst_offset, output_bytes, tap_count, spans_two_vectors; struct AreaTile *shared_pattern; } __attribute__((aligned(128))) AreaTile; // Cubic intermediates are int16_t: src_base counts components, but the gather // indices still address bytes. One tile produces up to 64 output components. typedef struct CubicTile { uint8_t indices[4][128], delta_controls[4][2][128]; int16_t weights[4][64]; int use_delta[4], single_vector_delta, src_base, dst_offset, output_bytes, tap_count, spans_two_vectors; struct CubicTile *shared_pattern; } __attribute__((aligned(128))) CubicTile; // P010 filtering uses signed 16-bit rows and 32-bit accumulators. A tile // gathers up to 64 components from at most two vectors of intermediate data. typedef struct { uint8_t indices[FILTER_MAX_TAPS][128]; int16_t weights[FILTER_MAX_TAPS][64]; int src_base, dst_offset, components, tap_count, spans_two_vectors; } __attribute__((aligned(128))) WideTile; #define ROTATION_MAX_ROWS 128 #define TRANSPOSE_WORKER_BYTES (2 * ROTATION_MAX_ROWS * 128) typedef struct { uint8_t *data; unsigned bytes, vtcm_context; } RotationBuffer; static void release_rotation_buffer(RotationBuffer *buffer) { if (buffer->vtcm_context) { qurt_mem_syncht(); compute_resource_release(buffer->vtcm_context); } else free(buffer->data); memset(buffer, 0, sizeof(*buffer)); } static int resize_rotation_buffer(RotationBuffer *buffer, unsigned bytes, int use_vtcm) { if (buffer->bytes == bytes && (!buffer->vtcm_context || use_vtcm)) return 0; // Workers are idle here. Release old storage before requesting scarce VTCM. release_rotation_buffer(buffer); if (!bytes) return 0; if (use_vtcm && compute_resource_attr_init && compute_resource_attr_set_vtcm_param && compute_resource_acquire && compute_resource_attr_get_vtcm_ptr && compute_resource_release) { compute_res_attr_t attr; if (!compute_resource_attr_init(&attr) && !compute_resource_attr_set_vtcm_param(&attr, bytes, 0)) { // Never wait for other clients or request exclusive access to the DSP. unsigned context = compute_resource_acquire(&attr, 0); if (context) { uint8_t *data = compute_resource_attr_get_vtcm_ptr(&attr); if (data && !((uintptr_t)data & 127)) { buffer->data = data; buffer->vtcm_context = context; } else compute_resource_release(context); } } } // Missing firmware APIs and exhausted VTCM both use ordinary aligned memory. // Retain that choice until the layout changes, instead of retrying every frame. if (!buffer->data) buffer->data = memalign(128, bytes); if (!buffer->data) return AEE_ENOMEMORY; buffer->bytes = bytes; return 0; } typedef struct ScalerState ScalerState; typedef struct { ScalerState *state; int worker_id, status; qurt_thread_t thread_id; qurt_sem_t work, done; void *stack; } Worker; // One separable pass per axis: vertical filtering builds a worker-private row, // then horizontal filtering emits it. Plane 0 is Y; plane 1 is interleaved UV. struct ScalerState { int src_width, src_height, dst_width, dst_height; Filter *horizontal_filters[2], *vertical_filters[2]; uint8_t *area_rows; AreaTile *area_tiles[2]; int area_tile_count[2]; CubicTile *cubic_tiles[2]; int cubic_tile_count[2]; int16_t *cubic_rows, *wide_rows; WideTile *wide_tiles[2]; int wide_tile_count[2], src_format, dst_format; RotationBuffer rotation_rows, transpose_workspace; int rotation, rotation_stride, rotation_worker_rows; Worker workers[CDSP_VPP_MAX_WORKERS]; // Spare workers stay asleep. int created_workers, stop; const uint8_t *src; uint8_t *dst; int src_stride, dst_stride, src_y_offset, src_uv_offset, dst_y_offset, dst_uv_offset, worker_count; int overlay_job; OverlayParams overlay; const uint8_t *rgba; int profiling; uint64_t profile_us[CDSP_VPP_PROFILE_COUNTERS]; // Per worker: Y/UV vertical/horizontal. }; static void worker_main(void *opaque); // Per-worker byte strides: vector-aligned rows plus padding for full-vector // gathers. Cubic rows need a leading border vector as well as trailing padding. static inline int area_row_stride_bytes(int width) { return (width + 383) & ~127; } static inline int cubic_row_stride_bytes(int width) { return (2 * width + 511) & ~127; } // Route each requested byte backwards through the butterfly network. Conflicts // fall back to LUT; duplicates are valid when the shared path agrees. static int build_area_delta_route(AreaTile *tile, int tap, int reverse) { uint8_t used[2][128] = {{0}}; int conflict_free = 1; for (int i = 0; i < tile->output_bytes; i++) if (tile->weights[tap][i]) { int source_byte = tile->indices[tap][i], table = source_byte / 128, node = i; for (int j = 0; j < 7; j++) { int bit = reverse ? (64 >> j) : (1 << j), switch_bit = (node ^ source_byte) & bit; if ((used[table][node] & bit) && ((tile->delta_controls[tap][table][node] & bit) != switch_bit)) conflict_free = 0; used[table][node] |= bit; tile->delta_controls[tap][table][node] |= switch_bit; node ^= switch_bit; } } return conflict_free; } int iris_vpp_open(const char *uri, remote_handle64 *h) { (void)uri; ScalerState *state = calloc(1, sizeof(*state)); if (!state) return AEE_ENOMEMORY; for (int i = 0; i < CDSP_VPP_MAX_WORKERS; i++) { Worker *worker = &state->workers[i]; worker->state = state; worker->worker_id = i; worker->stack = memalign(128, 8192); if (!worker->stack) { iris_vpp_close((uintptr_t)state); return AEE_ENOMEMORY; } qurt_sem_init_val(&worker->work, 0); qurt_sem_init_val(&worker->done, 0); qurt_thread_attr_t attributes; qurt_thread_attr_init(&attributes); qurt_thread_attr_set_stack_addr(&attributes, worker->stack); qurt_thread_attr_set_stack_size(&attributes, 8192); qurt_thread_attr_set_priority(&attributes, QURT_THREAD_ATTR_PRIORITY_DEFAULT / 2); qurt_thread_attr_set_name(&attributes, "vpp-worker"); if (qurt_thread_create(&worker->thread_id, &attributes, worker_main, worker) != QURT_EOK) { qurt_sem_destroy(&worker->work); qurt_sem_destroy(&worker->done); free(worker->stack); iris_vpp_close((uintptr_t)state); return AEE_EQURTTHREADCREATE; } state->created_workers++; } *h = (uintptr_t)state; return 0; } static void clear_filters(ScalerState *state) { for (int plane_index = 0; plane_index < 2; plane_index++) { free(state->horizontal_filters[plane_index]); free(state->vertical_filters[plane_index]); free(state->area_tiles[plane_index]); free(state->cubic_tiles[plane_index]); free(state->wide_tiles[plane_index]); state->wide_tiles[plane_index] = 0; state->wide_tile_count[plane_index] = 0; state->cubic_tiles[plane_index] = 0; state->area_tiles[plane_index] = 0; state->horizontal_filters[plane_index] = state->vertical_filters[plane_index] = 0; } release_rotation_buffer(&state->rotation_rows); release_rotation_buffer(&state->transpose_workspace); state->rotation_stride = 0; free(state->wide_rows); state->wide_rows = 0; free(state->area_rows); free(state->cubic_rows); state->cubic_rows = 0; state->area_rows = 0; state->src_width = 0; } int iris_vpp_close(remote_handle64 h) { ScalerState *state = (ScalerState *)(uintptr_t)h; state->stop = 1; for (int i = 0; i < state->created_workers; i++) qurt_sem_up(&state->workers[i].work); for (int i = 0; i < state->created_workers; i++) { Worker *worker = &state->workers[i]; int status; qurt_thread_join(worker->thread_id, &status); qurt_sem_destroy(&worker->work); qurt_sem_destroy(&worker->done); free(worker->stack); } clear_filters(state); HAP_power_destroy_client(state); free(state); return 0; } int iris_vpp_power(remote_handle64 h, int *mask) { ScalerState *state = (ScalerState *)(uintptr_t)h; HAP_power_request_t r; int e; *mask = 0; memset(&r, 0, sizeof(r)); r.type = HAP_power_set_apptype; r.apptype = HAP_POWER_COMPUTE_CLIENT_CLASS; if ((e = HAP_power_set(state, &r))) return e; *mask |= 1; memset(&r, 0, sizeof(r)); r.type = HAP_power_set_DCVS_v3; r.dcvs_v3.set_dcvs_enable = TRUE; r.dcvs_v3.dcvs_enable = TRUE; r.dcvs_v3.dcvs_option = HAP_DCVS_V2_PERFORMANCE_MODE; r.dcvs_v3.set_core_params = TRUE; r.dcvs_v3.core_params.min_corner = HAP_DCVS_VCORNER_MAX; r.dcvs_v3.core_params.max_corner = HAP_DCVS_VCORNER_MAX; r.dcvs_v3.core_params.target_corner = HAP_DCVS_VCORNER_MAX; r.dcvs_v3.set_bus_params = TRUE; r.dcvs_v3.bus_params.min_corner = HAP_DCVS_VCORNER_MAX; r.dcvs_v3.bus_params.max_corner = HAP_DCVS_VCORNER_MAX; r.dcvs_v3.bus_params.target_corner = HAP_DCVS_VCORNER_MAX; r.dcvs_v3.set_sleep_disable = TRUE; r.dcvs_v3.sleep_disable = HAP_DCVS_LPM_LEVEL1; if ((e = HAP_power_set(state, &r))) return e; *mask |= 2; memset(&r, 0, sizeof(r)); r.type = HAP_power_set_HVX; r.hvx.power_up = TRUE; if ((e = HAP_power_set(state, &r))) return e; *mask |= 4; return 0; } int iris_vpp_info(remote_handle64 h, unsigned int *hz, int *hvx_units) { (void)h; HAP_power_response_t r; memset(&r, 0, sizeof(r)); r.type = HAP_power_get_clk_Freq; int e = HAP_power_get(0, &r); if (e) return e; *hz = r.clkFreqHz; *hvx_units = (qurt_hvx_get_units() >> 8) & 255; return 0; } int iris_vpp_profile(remote_handle64 h, int enable, uint64_t *counters, int counter_count) { ScalerState *state = (ScalerState *)(uintptr_t)h; if (!counters || counter_count != CDSP_VPP_PROFILE_COUNTERS || (enable != 0 && enable != 1)) return AEE_EBADPARM; memcpy(counters, state->profile_us, sizeof(state->profile_us)); state->profiling = enable; return 0; } int iris_vpp_configure(remote_handle64 h, int src_width, int src_height, int dst_width, int dst_height) { ScalerState *state = (ScalerState *)(uintptr_t)h; if (src_width < 2 || dst_width < 2 || src_height < 2 || dst_height < 2 || (src_width | dst_width) & 1 || (src_height | dst_height) & 1 || src_width > 8192 || src_height > 8192 || dst_width > 8192 || dst_height > 8192 || src_width > dst_width * 20 || src_height > dst_height * 20 || !((src_width >= dst_width && src_height >= dst_height) || (src_width <= dst_width && src_height <= dst_height))) return AEE_EBADPARM; clear_filters(state); state->src_width = src_width; state->src_height = src_height; state->dst_width = dst_width; state->dst_height = dst_height; state->area_rows = memalign(128, CDSP_VPP_MAX_WORKERS * area_row_stride_bytes(src_width)); if (src_width < dst_width || src_height < dst_height) { state->cubic_rows = memalign(128, CDSP_VPP_MAX_WORKERS * cubic_row_stride_bytes(src_width)); if (!state->cubic_rows) { clear_filters(state); return AEE_ENOMEMORY; } memset(state->cubic_rows, 0, CDSP_VPP_MAX_WORKERS * cubic_row_stride_bytes(src_width)); } for (int plane_index = 0; plane_index < 2; plane_index++) { // NV12 UV has half as many samples in each axis, with two bytes per // sample. The channel offset below keeps U and V from being mixed. int channels = plane_index ? 2 : 1; state->horizontal_filters[plane_index] = malloc(dst_width / channels * sizeof(Filter)); state->vertical_filters[plane_index] = malloc(dst_height / channels * sizeof(Filter)); if (!state->horizontal_filters[plane_index] || !state->vertical_filters[plane_index] || !state->area_rows) { clear_filters(state); return AEE_ENOMEMORY; } for (int x = 0; x < dst_width / channels; x++) state->horizontal_filters[plane_index][x] = make_filter(src_width / channels, dst_width / channels, x); for (int y = 0; y < dst_height / channels; y++) state->vertical_filters[plane_index][y] = make_filter(src_height / channels, dst_height / channels, y); if (state->cubic_rows) { state->cubic_tile_count[plane_index] = (dst_width + 63) / 64; state->cubic_tiles[plane_index] = memalign(128, state->cubic_tile_count[plane_index] * sizeof(CubicTile)); if (!state->cubic_tiles[plane_index]) { clear_filters(state); return AEE_ENOMEMORY; } memset(state->cubic_tiles[plane_index], 0, state->cubic_tile_count[plane_index] * sizeof(CubicTile)); for (int tile_index = 0; tile_index < state->cubic_tile_count[plane_index]; tile_index++) { CubicTile *tile = &state->cubic_tiles[plane_index][tile_index]; tile->dst_offset = tile_index * 64; tile->output_bytes = dst_width - tile->dst_offset; if (tile->output_bytes > 64) tile->output_bytes = 64; tile->src_base = state->horizontal_filters[plane_index][tile->dst_offset / channels].indices[0] * channels; for (int i = 0; i < tile->output_bytes; i++) { Filter *filter = &state->horizontal_filters[plane_index][(tile->dst_offset + i) / channels]; if (filter->tap_count > tile->tap_count) tile->tap_count = filter->tap_count; for (int k = 0; k < filter->tap_count; k++) { int source_byte = (filter->indices[k] * channels + i % channels - tile->src_base) * 2; if (source_byte > 254) { clear_filters(state); return AEE_EBADPARM; } tile->indices[k][2 * i] = source_byte; tile->indices[k][2 * i + 1] = source_byte + 1; tile->weights[k][i] = filter->weights[k]; if (source_byte >= 128) tile->spans_two_vectors = 1; } } for (int k = 0; k < tile->tap_count; k++) { uint8_t used[2][128] = {{0}}; int conflict_free = 1; for (int i = 0; i < tile->output_bytes * 2; i++) if (tile->weights[k][i / 2]) { int source_byte = tile->indices[k][i], table = source_byte / 128, node = i; for (int bit = 1; bit <= 64; bit *= 2) { int switch_bit = (node ^ source_byte) & bit; if ((used[table][node] & bit) && ((tile->delta_controls[k][table][node] & bit) != switch_bit)) conflict_free = 0; used[table][node] |= bit; tile->delta_controls[k][table][node] |= switch_bit; node ^= switch_bit; } } tile->use_delta[k] = conflict_free; } tile->single_vector_delta = tile->tap_count == 4 && !tile->spans_two_vectors && tile->use_delta[0] && tile->use_delta[1] && tile->use_delta[2] && tile->use_delta[3]; tile->shared_pattern = tile; for (int j = 0; j < tile_index; j++) { CubicTile *pattern = &state->cubic_tiles[plane_index][j]; if (pattern->shared_pattern == pattern && pattern->tap_count == tile->tap_count && pattern->output_bytes == tile->output_bytes && pattern->spans_two_vectors == tile->spans_two_vectors && !memcmp(pattern->indices, tile->indices, tile->tap_count * 128) && !memcmp(pattern->weights, tile->weights, tile->tap_count * 128)) { tile->shared_pattern = pattern; break; } } } continue; } // Keep a tile's source footprint within 256 bytes. Noninteger ratios // need margin for fractional endpoints; even tile sizes preserve UV pairs. int tile_bytes = src_width > dst_width ? ((src_width % dst_width ? 252 : 256) * dst_width / src_width) & ~1 : 128; if (tile_bytes < 2) tile_bytes = 2; if (tile_bytes > 128) tile_bytes = 128; state->area_tile_count[plane_index] = (dst_width + tile_bytes - 1) / tile_bytes; state->area_tiles[plane_index] = memalign(128, state->area_tile_count[plane_index] * sizeof(AreaTile)); if (!state->area_tiles[plane_index]) { clear_filters(state); return AEE_ENOMEMORY; } memset(state->area_tiles[plane_index], 0, state->area_tile_count[plane_index] * sizeof(AreaTile)); for (int tile_index = 0; tile_index < state->area_tile_count[plane_index]; tile_index++) { AreaTile *tile = &state->area_tiles[plane_index][tile_index]; tile->dst_offset = tile_index * tile_bytes; tile->output_bytes = dst_width - tile->dst_offset; if (tile->output_bytes > tile_bytes) tile->output_bytes = tile_bytes; tile->src_base = state->horizontal_filters[plane_index][tile->dst_offset / channels].indices[0] * channels; int max_source_byte = 0; for (int i = 0; i < tile->output_bytes; i++) { Filter *filter = &state->horizontal_filters[plane_index][(tile->dst_offset + i) / channels]; if (filter->tap_count > tile->tap_count) tile->tap_count = filter->tap_count; for (int k = 0; k < filter->tap_count; k++) { int index = filter->indices[k] * channels + i % channels - tile->src_base; if (index > 255) { clear_filters(state); return AEE_EBADPARM; } tile->indices[k][i] = index; tile->weights[k][i] = filter->weights[k]; if (index > max_source_byte) max_source_byte = index; } } tile->spans_two_vectors = max_source_byte >= 128; for (int k = 0; k < tile->tap_count; k++) tile->use_delta[k] = build_area_delta_route(tile, k, src_width > dst_width); // Rational ratios repeat the same lane pattern. Share its hot // coefficient/routing cache lines across spatial tiles. tile->shared_pattern = tile; for (int j = 0; j < tile_index; j++) { AreaTile *pattern = &state->area_tiles[plane_index][j]; if (pattern->shared_pattern != pattern || pattern->tap_count != tile->tap_count || pattern->spans_two_vectors != tile->spans_two_vectors || pattern->output_bytes != tile->output_bytes) continue; if (!memcmp(pattern->indices, tile->indices, tile->tap_count * 128) && !memcmp(pattern->weights, tile->weights, tile->tap_count * 128)) { tile->shared_pattern = pattern; break; } } } } return 0; } // P010 reduction uses wide tiles; enlargement shares the cubic routing cache. // Allocate a signed intermediate row only when a P010 operation resamples. static int prepare_wide_filters(ScalerState *state) { if (state->wide_rows) return 0; for (int plane = 0; plane < 2 && !state->cubic_rows; plane++) { int channels = plane ? 2 : 1; int capacity = 0; for (int x = 0; x < state->dst_width;) { Filter *first = &state->horizontal_filters[plane][x / channels]; int base = first->indices[0] * channels; int count = 0; while (count + channels <= 64 && x + count < state->dst_width) { Filter *filter = &state->horizontal_filters[plane][(x + count) / channels]; int last = (filter->indices[filter->tap_count - 1] + 1) * channels; if (last - base > 128) break; count += channels; } if (!count) goto fail; if (state->wide_tile_count[plane] == capacity) { int next = capacity ? capacity * 2 : 16; WideTile *tiles = memalign(128, next * sizeof(*tiles)); if (!tiles) goto fail; if (capacity) memcpy(tiles, state->wide_tiles[plane], capacity * sizeof(*tiles)); free(state->wide_tiles[plane]); state->wide_tiles[plane] = tiles; capacity = next; } WideTile *tile = &state->wide_tiles[plane][state->wide_tile_count[plane]++]; memset(tile, 0, sizeof(*tile)); tile->src_base = base; tile->dst_offset = x; tile->components = count; for (int i = 0; i < count; i++) { Filter *filter = &state->horizontal_filters[plane][(x + i) / channels]; if (filter->tap_count > tile->tap_count) tile->tap_count = filter->tap_count; for (int k = 0; k < filter->tap_count; k++) { int byte = 2 * (filter->indices[k] * channels + i % channels - base); tile->indices[k][2 * i] = byte; tile->indices[k][2 * i + 1] = byte + 1; tile->weights[k][i] = filter->weights[k]; tile->spans_two_vectors |= byte >= 128; } } x += count; } } state->wide_rows = memalign(128, CDSP_VPP_MAX_WORKERS * cubic_row_stride_bytes(state->src_width)); if (!state->wide_rows) goto fail; memset(state->wide_rows, 0, CDSP_VPP_MAX_WORKERS * cubic_row_stride_bytes(state->src_width)); return 0; fail: for (int plane = 0; plane < 2; plane++) { free(state->wide_tiles[plane]); state->wide_tiles[plane] = 0; state->wide_tile_count[plane] = 0; } return AEE_ENOMEMORY; } typedef HVX_Vector UnalignedVector __attribute__((aligned(1))); // Store 1..128 live bytes without issuing an all-false store beyond the // buffer's last page. vsetq masks its count to seven bits: 128 is not all-ones. static void store_partial_vector(uint8_t *dst, HVX_Vector value, int bytes) { int offset = (uintptr_t)dst & 127, end = offset + bytes; HVX_Vector *aligned = (HVX_Vector *)((uintptr_t)dst & ~(uintptr_t)127); value = Q6_V_vror_VR(value, (128 - offset) & 127); HVX_VectorPred mask = Q6_Q_not_Q(Q6_Q_vsetq_R(offset)); if (end < 128) mask = Q6_Q_and_QQ(mask, Q6_Q_vsetq_R(end)); Q6_vmem_QRIV(mask, aligned, value); if (end > 128) Q6_vmem_QRIV(Q6_Q_vsetq_R(end - 128), aligned + 1, value); } #include "overlay.h" // vlut32 selects 32-byte banks. Its inputs are byte-shuffled by the caller; // delta routing is the faster path when all requested lanes are conflict-free. static inline HVX_Vector gather_bytes(HVX_Vector indices, HVX_Vector lower, HVX_Vector upper, int spans_two_vectors) { HVX_Vector gathered = Q6_Vb_vlut32_VbVbI(indices, lower, 0); gathered = Q6_Vb_vlut32or_VbVbVbI(gathered, indices, lower, 1); gathered = Q6_Vb_vlut32or_VbVbVbI(gathered, indices, lower, 2); gathered = Q6_Vb_vlut32or_VbVbVbI(gathered, indices, lower, 3); if (spans_two_vectors) { gathered = Q6_Vb_vlut32or_VbVbVbI(gathered, indices, upper, 4); gathered = Q6_Vb_vlut32or_VbVbVbI(gathered, indices, upper, 5); gathered = Q6_Vb_vlut32or_VbVbVbI(gathered, indices, upper, 6); gathered = Q6_Vb_vlut32or_VbVbVbI(gathered, indices, upper, 7); } return gathered; } static void area_horizontal(ScalerState *state, int plane_index, uint8_t *dst, uint8_t *row) { for (int tile_index = 0; tile_index < state->area_tile_count[plane_index]; tile_index++) { AreaTile *tile = &state->area_tiles[plane_index][tile_index]; AreaTile *pattern = tile->shared_pattern; HVX_Vector lower = *(const UnalignedVector *)(row + tile->src_base); HVX_Vector upper = *(const UnalignedVector *)(row + tile->src_base + 128); HVX_VectorPair accumulator = Q6_W_vcombine_VV(Q6_V_vzero(), Q6_V_vzero()); for (int k = 0; k < tile->tap_count; k++) { HVX_Vector indices = *(HVX_Vector *)pattern->indices[k], samples; if (pattern->use_delta[k]) { HVX_Vector lower_control = *(HVX_Vector *)pattern->delta_controls[k][0], upper_control = *(HVX_Vector *)pattern->delta_controls[k][1]; HVX_Vector lower_samples = state->src_width > state->dst_width ? Q6_V_vrdelta_VV(lower, lower_control) : Q6_V_vdelta_VV(lower, lower_control); if (tile->spans_two_vectors) { HVX_Vector upper_samples = state->src_width > state->dst_width ? Q6_V_vrdelta_VV(upper, upper_control) : Q6_V_vdelta_VV(upper, upper_control); samples = Q6_V_vmux_QVV(Q6_Q_vcmp_gt_VubVub(indices, Q6_V_vsplat_R(0x7f7f7f7f)), upper_samples, lower_samples); } else samples = lower_samples; } else samples = gather_bytes(indices, Q6_Vb_vshuff_Vb(lower), Q6_Vb_vshuff_Vb(upper), tile->spans_two_vectors); accumulator = state->src_width >= state->dst_width ? Q6_Wuh_vmpyacc_WuhVubVub(accumulator, samples, *(HVX_Vector *)pattern->weights[k]) : Q6_Wh_vmpyacc_WhVubVb(accumulator, samples, *(HVX_Vector *)pattern->weights[k]); } HVX_Vector out = Q6_Vub_vasr_VhVhR_rnd_sat(Q6_V_hi_W(accumulator), Q6_V_lo_W(accumulator), state->src_width >= state->dst_width ? 7 : 6); if (tile->output_bytes == 128) *(UnalignedVector *)(dst + tile->dst_offset) = out; else { uint8_t tmp[128] __attribute__((aligned(128))); *(HVX_Vector *)tmp = out; memcpy(dst + tile->dst_offset, tmp, tile->output_bytes); } } } static void area_vertical(const uint8_t *src, int stride, uint8_t *row, int width, Filter *filter, int hvx) { int x = 0; if (hvx) for (; x + 128 <= width; x += 128) { HVX_VectorPair sum = Q6_W_vcombine_VV(Q6_V_vzero(), Q6_V_vzero()); for (int k = 0; k < filter->tap_count; k++) { HVX_Vector samples = *(const UnalignedVector *)(src + filter->indices[k] * stride + x); int packed_weight = (uint8_t)filter->weights[k] * 0x01010101; sum = filter->shift == 7 ? Q6_Wuh_vmpyacc_WuhVubRub(sum, samples, packed_weight) : Q6_Wh_vmpyacc_WhVubRb(sum, samples, packed_weight); } *(HVX_Vector *)(row + x) = Q6_Vub_vasr_VhVhR_rnd_sat(Q6_V_hi_W(sum), Q6_V_lo_W(sum), filter->shift); } for (; x < width; x++) { int sum = 0; for (int k = 0; k < filter->tap_count; k++) sum += src[filter->indices[k] * stride + x] * filter->weights[k]; row[x] = round_clip_u8(sum, filter->shift); } } // Preserve signed Q6 intermediates, including cubic undershoot/overshoot. // Q14 coefficients and 32-bit accumulators avoid early clipping and phase loss. // Keep vertical kernels out of the row dispatcher to limit register pressure. static __attribute__((noinline)) void cubic_vertical(const uint8_t *src, int stride, int16_t *restrict row, int width, const Filter *filter, int hvx) { int x = 0, shift = filter->shift - 6; if (hvx && width >= 128 && filter->tap_count == 4) { const uint8_t *lines[4]; int weights[4]; for (int k = 0; k < 4; k++) { lines[k] = src + filter->indices[k] * stride; weights[k] = (uint32_t)(uint16_t)filter->weights[k] * 0x00010001u; } for (;;) { // Recompute the last full vector instead of filtering a scalar // tail. The overlapping read and write stay within this row. if (x + 128 > width) x = width - 128; HVX_VectorPair even = Q6_W_vcombine_VV(Q6_V_vzero(), Q6_V_vzero()), odd = even; #pragma clang loop unroll(full) for (int k = 0; k < 4; k++) { HVX_VectorPair widened = Q6_Wuh_vzxt_Vub(*(const UnalignedVector *)(lines[k] + x)); even = Q6_Ww_vmpyacc_WwVhRh(even, Q6_V_lo_W(widened), weights[k]); odd = Q6_Ww_vmpyacc_WwVhRh(odd, Q6_V_hi_W(widened), weights[k]); } HVX_Vector even_samples = Q6_Vh_vasr_VwVwR_rnd_sat(Q6_V_hi_W(even), Q6_V_lo_W(even), shift); HVX_Vector odd_samples = Q6_Vh_vasr_VwVwR_rnd_sat(Q6_V_hi_W(odd), Q6_V_lo_W(odd), shift); HVX_VectorPair v = Q6_W_vshuff_VVR(odd_samples, even_samples, -2); *(UnalignedVector *)(row + x) = Q6_V_lo_W(v); *(UnalignedVector *)(row + x + 64) = Q6_V_hi_W(v); x += 128; if (x == width) break; } } else if (hvx) for (; x + 128 <= width; x += 128) { HVX_VectorPair even = Q6_W_vcombine_VV(Q6_V_vzero(), Q6_V_vzero()), odd = even; for (int k = 0; k < filter->tap_count; k++) { HVX_Vector v = *(const UnalignedVector *)(src + filter->indices[k] * stride + x); HVX_VectorPair widened = Q6_Wuh_vzxt_Vub(v); int packed_weight = (uint32_t)(uint16_t)filter->weights[k] * 0x00010001u; even = Q6_Ww_vmpyacc_WwVhRh(even, Q6_V_lo_W(widened), packed_weight); odd = Q6_Ww_vmpyacc_WwVhRh(odd, Q6_V_hi_W(widened), packed_weight); } HVX_Vector even_samples = Q6_Vh_vasr_VwVwR_rnd_sat(Q6_V_hi_W(even), Q6_V_lo_W(even), shift); HVX_Vector odd_samples = Q6_Vh_vasr_VwVwR_rnd_sat(Q6_V_hi_W(odd), Q6_V_lo_W(odd), shift); HVX_VectorPair v = Q6_W_vshuff_VVR(odd_samples, even_samples, -2); *(HVX_Vector *)(row + x) = Q6_V_lo_W(v); *(HVX_Vector *)(row + x + 64) = Q6_V_hi_W(v); } for (; x < width; x++) { int sum = 0; for (int k = 0; k < filter->tap_count; k++) sum += src[filter->indices[k] * stride + x] * filter->weights[k]; row[x] = (sum + (1 << (shift - 1))) >> shift; } } // Exact 2x Catmull-Rom has two fixed Q7 phases. The generic Q14 // coefficients are exactly 128 times these integers, so this is bit exact // even with signed Q6 vertical intermediates. static void cubic_vertical_2x(const uint8_t *src, int stride, int16_t *row, int width, const Filter *filter) { int x = 0; HVX_Vector bias = Q6_V_vsplat_R(0xc000c000u); // -128 * 128 HVX_Vector restore = Q6_V_vsplat_R(0x20002000); // +128 in Q6 for (; x + 128 <= width; x += 128) { // Center the sum before multiplying. With positive weights summing to // 140 and negative weights to -12, every partial sum fits signed 16 bits. HVX_VectorPair accumulator = Q6_W_vcombine_VV(bias, bias); for (int k = 0; k < 4; k++) { HVX_Vector v = *(const UnalignedVector *)(src + filter->indices[k] * stride + x); uint32_t packed_weight = (uint8_t)(filter->weights[k] / 128) * 0x01010101u; accumulator = Q6_Wh_vmpyacc_WhVubRb(accumulator, v, packed_weight); } HVX_Vector even = Q6_Vh_vadd_VhVh(Q6_Vh_vavg_VhVh_rnd(Q6_V_lo_W(accumulator), Q6_V_vzero()), restore); HVX_Vector odd = Q6_Vh_vadd_VhVh(Q6_Vh_vavg_VhVh_rnd(Q6_V_hi_W(accumulator), Q6_V_vzero()), restore); HVX_VectorPair v = Q6_W_vshuff_VVR(odd, even, -2); *(HVX_Vector *)(row + x) = Q6_V_lo_W(v); *(HVX_Vector *)(row + x + 64) = Q6_V_hi_W(v); } for (; x < width; x++) { int sum = 0; for (int k = 0; k < 4; k++) sum += src[filter->indices[k] * stride + x] * filter->weights[k]; row[x] = (sum + 128) >> 8; } } static void cubic_horizontal_2x(ScalerState *state, int plane_index, uint8_t *dst, int16_t *row) { int channels = plane_index ? 2 : 1; int p010 = state->dst_format == CDSP_VPP_P010; int shift = p010 ? 11 : 13; // Extend both borders independently per channel, then evaluate the two Q7 // phases [-3, 29, 111, -9] and [-9, 111, 29, -3]. The row is 8-bit Q6 // (equivalently 10-bit Q4); round directly to the destination bit depth. for (int j = 0; j < 2 * channels; j++) { row[j - 2 * channels] = row[j % channels]; row[state->src_width + j] = row[state->src_width - channels + j % channels]; } for (int x = 0; x < state->src_width; x += 64) { HVX_Vector left2 = *(const UnalignedVector *)(row + x - 2 * channels); HVX_Vector left1 = *(const UnalignedVector *)(row + x - channels); HVX_Vector center = *(const UnalignedVector *)(row + x); HVX_Vector right1 = *(const UnalignedVector *)(row + x + channels); HVX_Vector right2 = *(const UnalignedVector *)(row + x + 2 * channels); HVX_VectorPair even = Q6_W_vcombine_VV(Q6_V_vzero(), Q6_V_vzero()); HVX_VectorPair odd = even; even = Q6_Ww_vmpyacc_WwVhRh(even, left2, (int)0xfffdfffdu); // -3 even = Q6_Ww_vmpyacc_WwVhRh(even, left1, 0x001d001d); // 29 even = Q6_Ww_vmpyacc_WwVhRh(even, center, 0x006f006f); // 111 even = Q6_Ww_vmpyacc_WwVhRh(even, right1, (int)0xfff7fff7u); // -9 odd = Q6_Ww_vmpyacc_WwVhRh(odd, left1, (int)0xfff7fff7u); odd = Q6_Ww_vmpyacc_WwVhRh(odd, center, 0x006f006f); odd = Q6_Ww_vmpyacc_WwVhRh(odd, right1, 0x001d001d); odd = Q6_Ww_vmpyacc_WwVhRh(odd, right2, (int)0xfffdfffdu); HVX_Vector even_samples = Q6_Vh_vasr_VwVwR_rnd_sat(Q6_V_hi_W(even), Q6_V_lo_W(even), shift); HVX_Vector odd_samples = Q6_Vh_vasr_VwVwR_rnd_sat(Q6_V_hi_W(odd), Q6_V_lo_W(odd), shift); HVX_VectorPair interleaved = Q6_W_vshuff_VVR(odd_samples, even_samples, plane_index ? -4 : -2); int components = 2 * (state->src_width - x); if (p010) { HVX_Vector lo = Q6_Vh_vmax_VhVh(Q6_V_lo_W(interleaved), Q6_V_vzero()); HVX_Vector hi = Q6_Vh_vmax_VhVh(Q6_V_hi_W(interleaved), Q6_V_vzero()); lo = Q6_Vh_vasl_VhR(Q6_Vh_vmin_VhVh(lo, Q6_V_vsplat_R(0x03ff03ff)), 6); hi = Q6_Vh_vasl_VhR(Q6_Vh_vmin_VhVh(hi, Q6_V_vsplat_R(0x03ff03ff)), 6); if (components >= 128) { *(UnalignedVector *)(dst + 4 * x) = lo; *(UnalignedVector *)(dst + 4 * x + 128) = hi; } else { uint8_t tail[256] __attribute__((aligned(128))); *(HVX_Vector *)tail = lo; *(HVX_Vector *)(tail + 128) = hi; memcpy(dst + 4 * x, tail, 2 * components); } } else { HVX_Vector out = Q6_Vub_vpack_VhVh_sat(Q6_V_hi_W(interleaved), Q6_V_lo_W(interleaved)); if (components >= 128) *(UnalignedVector *)(dst + 2 * x) = out; else { uint8_t tail[128] __attribute__((aligned(128))); *(HVX_Vector *)tail = out; memcpy(dst + 2 * x, tail, components); } } } } // NV12 Q6 and 10-bit Q4 intermediates share a numeric scale. Reuse the // gather patterns for both formats and round directly to the output bit depth. static void cubic_horizontal(ScalerState *state, int plane_index, uint8_t *dst, int16_t *row) { if (state->dst_width == 2 * state->src_width) { cubic_horizontal_2x(state, plane_index, dst, row); return; } int shift = state->horizontal_filters[plane_index][0].shift; int p010 = state->dst_format == CDSP_VPP_P010; int extra = p010 ? 4 : 6; HVX_Vector previous = Q6_V_vzero(); for (int tile_index = 0; tile_index < state->cubic_tile_count[plane_index]; tile_index++) { CubicTile *tile = &state->cubic_tiles[plane_index][tile_index], *pattern = tile->shared_pattern; HVX_Vector lower = *(const UnalignedVector *)(row + tile->src_base), upper = *(const UnalignedVector *)((const uint8_t *)(row + tile->src_base) + 128); HVX_VectorPair accumulator = Q6_W_vcombine_VV(Q6_V_vzero(), Q6_V_vzero()); if (pattern->single_vector_delta) { // Upsampling usually gathers all four taps from one vector. Keep // that case branch-free inside the tap loop so LLVM can schedule // the independent gathers and coefficient loads together. #pragma clang loop unroll(full) for (int k = 0; k < 4; k++) { HVX_Vector samples = Q6_V_vdelta_VV(lower, *(HVX_Vector *)pattern->delta_controls[k][0]); accumulator = Q6_Ww_vmpyacc_WwVhVh(accumulator, samples, *(HVX_Vector *)pattern->weights[k]); } } else for (int k = 0; k < tile->tap_count; k++) { HVX_Vector indices = *(HVX_Vector *)pattern->indices[k], samples; if (pattern->use_delta[k]) { HVX_Vector lower_samples = Q6_V_vdelta_VV(lower, *(HVX_Vector *)pattern->delta_controls[k][0]); if (tile->spans_two_vectors) { HVX_Vector upper_samples = Q6_V_vdelta_VV(upper, *(HVX_Vector *)pattern->delta_controls[k][1]); samples = Q6_V_vmux_QVV(Q6_Q_vcmp_gt_VubVub(indices, Q6_V_vsplat_R(0x7f7f7f7f)), upper_samples, lower_samples); } else samples = lower_samples; } else samples = gather_bytes(indices, Q6_Vb_vshuff_Vb(lower), Q6_Vb_vshuff_Vb(upper), tile->spans_two_vectors); accumulator = Q6_Ww_vmpyacc_WwVhVh(accumulator, samples, *(HVX_Vector *)pattern->weights[k]); } // The narrowing instruction masks its shift count to four bits. Drop the // fractional intermediate bits first; the final rounding is unchanged. HVX_Vector narrowed = Q6_Vh_vasr_VwVwR_rnd_sat(Q6_Vw_vasr_VwR(Q6_V_hi_W(accumulator), extra), Q6_Vw_vasr_VwR(Q6_V_lo_W(accumulator), extra), shift); if (p010) { narrowed = Q6_Vh_vmax_VhVh(narrowed, Q6_V_vzero()); narrowed = Q6_Vh_vmin_VhVh(narrowed, Q6_V_vsplat_R(0x03ff03ff)); HVX_Vector out = Q6_Vh_vasl_VhR(narrowed, 6); uint8_t *target = dst + 2 * tile->dst_offset; if (tile->output_bytes == 64) *(UnalignedVector *)target = out; else store_partial_vector(target, out, 2 * tile->output_bytes); continue; } if (!(tile_index & 1) && tile_index + 1 < state->cubic_tile_count[plane_index]) { previous = narrowed; continue; } HVX_Vector out = (tile_index & 1) ? Q6_Vub_vpack_VhVh_sat(narrowed, previous) : Q6_Vub_vpack_VhVh_sat(Q6_V_vzero(), narrowed); int output_bytes = tile->output_bytes + ((tile_index & 1) ? 64 : 0); uint8_t *target = dst + tile->dst_offset - ((tile_index & 1) ? 64 : 0); if (output_bytes == 128) *(UnalignedVector *)target = out; else if (!((uintptr_t)target & 127)) Q6_vmem_QRIV(Q6_Q_vsetq_R(output_bytes), (HVX_Vector *)target, out); else { uint8_t tmp[128] __attribute__((aligned(128))); *(HVX_Vector *)tmp = out; memcpy(target, tmp, output_bytes); } } } static void scale_cubic_plane(ScalerState *state, const uint8_t *src, uint8_t *dst, int src_stride, int dst_stride, int plane_index, int hvx, int worker_id, int first_row, int end_row) { int channels = plane_index ? 2 : 1; int16_t *row = (int16_t *)((uint8_t *)state->cubic_rows + worker_id * cubic_row_stride_bytes(state->src_width) + 128); for (int y = first_row; y < end_row; y++) { if (hvx && src_stride <= 65535) { int ahead = y + 2; if (ahead >= state->dst_height / channels) ahead = y; Filter *filter = &state->vertical_filters[plane_index][ahead]; Q6_l2fetch_AP((void *)(src + filter->indices[0] * src_stride), ((uint64_t)src_stride << 32) | ((uint64_t)state->src_width << 16) | (filter->indices[filter->tap_count - 1] - filter->indices[0] + 1)); } uint64_t start = state->profiling ? HAP_perf_get_time_us() : 0; if (hvx && state->dst_height == 2 * state->src_height) cubic_vertical_2x(src, src_stride, row, state->src_width, &state->vertical_filters[plane_index][y]); else cubic_vertical(src, src_stride, row, state->src_width, &state->vertical_filters[plane_index][y], hvx); uint64_t middle = state->profiling ? HAP_perf_get_time_us() : 0; if (state->profiling) state->profile_us[worker_id * 4 + plane_index * 2] += middle - start; if (hvx) { cubic_horizontal(state, plane_index, dst + (y - first_row) * dst_stride, row); if (state->profiling) state->profile_us[worker_id * 4 + plane_index * 2 + 1] += HAP_perf_get_time_us() - middle; continue; } for (int x = 0; x < state->dst_width / channels; x++) for (int ch = 0; ch < channels; ch++) { Filter *filter = &state->horizontal_filters[plane_index][x]; int sum = 0; for (int k = 0; k < filter->tap_count; k++) sum += row[filter->indices[k] * channels + ch] * filter->weights[k]; dst[(y - first_row) * dst_stride + x * channels + ch] = round_clip_u8(sum, filter->shift + 6); } } } static void scale_plane(ScalerState *state, const uint8_t *src, uint8_t *dst, int src_stride, int dst_stride, int plane_index, int hvx, int worker_id, int first_row, int end_row) { if (state->cubic_rows) { scale_cubic_plane(state, src, dst, src_stride, dst_stride, plane_index, hvx, worker_id, first_row, end_row); return; } int channels = plane_index ? 2 : 1; uint8_t *row = state->area_rows + worker_id * area_row_stride_bytes(state->src_width); for (int y = first_row; y < end_row; y++) { if (hvx && src_stride <= 65535) { int ahead = y + 2; if (ahead >= state->dst_height / channels) ahead = y; Filter *filter = &state->vertical_filters[plane_index][ahead]; uint64_t config = ((uint64_t)src_stride << 32) | ((uint64_t)state->src_width << 16) | (filter->indices[filter->tap_count - 1] - filter->indices[0] + 1); Q6_l2fetch_AP((void *)(src + filter->indices[0] * src_stride), config); } if (hvx && state->src_width == state->dst_width && state->src_height == state->dst_height) { int x = 0; for (; x + 128 <= state->dst_width; x += 128) *(UnalignedVector *)(dst + (y - first_row) * dst_stride + x) = *(const UnalignedVector *)(src + y * src_stride + x); if (x < state->dst_width) memcpy(dst + (y - first_row) * dst_stride + x, src + y * src_stride + x, state->dst_width - x); continue; } if (hvx && state->src_width == 2 * state->dst_width && state->src_height == 2 * state->dst_height) { // Match separable area filtering: round the vertical averages first, // then the horizontal averages. UV packing preserves channel pairs. const uint8_t *r0 = src + 2 * y * src_stride, *r1 = r0 + src_stride; int x = 0; for (; x + 128 <= state->dst_width; x += 128) { HVX_Vector a = Q6_Vub_vavg_VubVub_rnd(*(const UnalignedVector *)(r0 + 2 * x), *(const UnalignedVector *)(r1 + 2 * x)); HVX_Vector b = Q6_Vub_vavg_VubVub_rnd(*(const UnalignedVector *)(r0 + 2 * x + 128), *(const UnalignedVector *)(r1 + 2 * x + 128)); HVX_Vector even = plane_index ? Q6_Vh_vpacke_VwVw(b, a) : Q6_Vb_vpacke_VhVh(b, a); HVX_Vector odd = plane_index ? Q6_Vh_vpacko_VwVw(b, a) : Q6_Vb_vpacko_VhVh(b, a); *(UnalignedVector *)(dst + (y - first_row) * dst_stride + x) = Q6_Vub_vavg_VubVub_rnd(even, odd); } if (x < state->dst_width) { // Copy only a partial vector's live input, never read past an // imported plane. The fast path above has no intermediate row. uint8_t t0[256] __attribute__((aligned(128))) = {0}; uint8_t t1[256] __attribute__((aligned(128))) = {0}; int output_bytes = state->dst_width - x; memcpy(t0, r0 + 2 * x, 2 * output_bytes); memcpy(t1, r1 + 2 * x, 2 * output_bytes); HVX_Vector a = Q6_Vub_vavg_VubVub_rnd(*(HVX_Vector *)t0, *(HVX_Vector *)t1); HVX_Vector b = Q6_Vub_vavg_VubVub_rnd(*(HVX_Vector *)(t0 + 128), *(HVX_Vector *)(t1 + 128)); HVX_Vector even = plane_index ? Q6_Vh_vpacke_VwVw(b, a) : Q6_Vb_vpacke_VhVh(b, a); HVX_Vector odd = plane_index ? Q6_Vh_vpacko_VwVw(b, a) : Q6_Vb_vpacko_VhVh(b, a); HVX_Vector out = Q6_Vub_vavg_VubVub_rnd(even, odd); uint8_t *target = dst + (y - first_row) * dst_stride + x; if (!((uintptr_t)target & 127)) Q6_vmem_QRIV(Q6_Q_vsetq_R(output_bytes), (HVX_Vector *)target, out); else { *(HVX_Vector *)t0 = out; memcpy(target, t0, output_bytes); } } continue; } area_vertical(src, src_stride, row, state->src_width, &state->vertical_filters[plane_index][y], hvx); if (hvx) { area_horizontal(state, plane_index, dst + (y - first_row) * dst_stride, row); continue; } for (int x = 0; x < state->dst_width / channels; x++) for (int ch = 0; ch < channels; ch++) { Filter *filter = &state->horizontal_filters[plane_index][x]; int a = 0; for (int k = 0; k < filter->tap_count; k++) a += row[filter->indices[k] * channels + ch] * filter->weights[k]; dst[(y - first_row) * dst_stride + x * channels + ch] = round_clip_u8(a, filter->shift); } } } // P010 stores ten significant bits in the high bits of little-endian words. // Preserve code values/range: 8->10 is a two-bit shift, 10->8 rounds and clips. static void convert_row(const uint8_t *src, uint8_t *dst, int width, int src_format, int dst_format, int hvx) { if (src_format == dst_format) { int bytes = width * (src_format == CDSP_VPP_P010 ? 2 : 1), x = 0; if (hvx) for (; x + 128 <= bytes; x += 128) *(UnalignedVector *)(dst + x) = *(const UnalignedVector *)(src + x); memcpy(dst + x, src + x, bytes - x); return; } int x = 0; if (hvx) for (; x + 128 <= width; x += 128) { if (src_format == CDSP_VPP_NV12) { HVX_VectorPair words = Q6_Wuh_vunpack_Vub(*(const UnalignedVector *)(src + x)); *(UnalignedVector *)(dst + 2 * x) = Q6_Vh_vasl_VhR(Q6_V_lo_W(words), 8); *(UnalignedVector *)(dst + 2 * x + 128) = Q6_Vh_vasl_VhR(Q6_V_hi_W(words), 8); } else { HVX_Vector lo = *(const UnalignedVector *)(src + 2 * x); HVX_Vector hi = *(const UnalignedVector *)(src + 2 * x + 128); // Saturating addition preserves the 1022/1023 -> 255 endpoint. HVX_Vector bias = Q6_V_vsplat_R(0x00800080); lo = Q6_Vuh_vadd_VuhVuh_sat(lo, bias); hi = Q6_Vuh_vadd_VuhVuh_sat(hi, bias); *(UnalignedVector *)(dst + x) = Q6_Vb_vpacko_VhVh(hi, lo); } } for (; x < width; x++) { if (src_format == CDSP_VPP_NV12) ((uint16_t *)dst)[x] = (uint16_t)src[x] << 8; else dst[x] = clamp_int((((const uint16_t *)src)[x] + 128) >> 8, 0, 255); } } static __attribute__((noinline)) void wide_vertical(const uint8_t *src, int stride, int16_t *restrict row, int width, const Filter *filter, int format, int hvx) { // Q4 keeps 10-bit cubic overshoot within int16_t. Area uses the same // precision so conversion to 8 bits happens only after both filter passes. int shift = filter->shift - 4, x = 0; if (hvx && format == CDSP_VPP_P010 && filter->tap_count == 4 && width >= 64) { const uint8_t *lines[4]; int weights[4]; for (int k = 0; k < 4; k++) { lines[k] = src + filter->indices[k] * stride; weights[k] = (uint32_t)(uint16_t)filter->weights[k] * 0x00010001u; } for (;;) { if (x + 64 > width) x = width - 64; HVX_VectorPair sum = Q6_W_vcombine_VV(Q6_V_vzero(), Q6_V_vzero()); #pragma clang loop unroll(full) for (int k = 0; k < 4; k++) { HVX_Vector samples = Q6_Vuh_vlsr_VuhR(*(const UnalignedVector *)(lines[k] + 2 * x), 6); sum = Q6_Ww_vmpyacc_WwVhRh(sum, samples, weights[k]); } *(UnalignedVector *)(row + x) = Q6_Vh_vasr_VwVwR_rnd_sat(Q6_V_hi_W(sum), Q6_V_lo_W(sum), shift); x += 64; if (x == width) break; } } else if (hvx) for (; x + 64 <= width; x += 64) { HVX_VectorPair sum = Q6_W_vcombine_VV(Q6_V_vzero(), Q6_V_vzero()); for (int k = 0; k < filter->tap_count; k++) { const uint8_t *line = src + filter->indices[k] * stride; HVX_Vector samples; if (format == CDSP_VPP_P010) samples = Q6_Vuh_vlsr_VuhR(*(const UnalignedVector *)(line + 2 * x), 6); else { HVX_Vector bytes; if (x + 128 <= width) bytes = *(const UnalignedVector *)(line + x); else { uint8_t tail[128] __attribute__((aligned(128))) = {0}; memcpy(tail, line + x, 64); bytes = *(HVX_Vector *)tail; } samples = Q6_Vh_vasl_VhR(Q6_V_lo_W(Q6_Wuh_vunpack_Vub(bytes)), 2); } uint32_t weight = (uint16_t)filter->weights[k] * 0x00010001u; sum = Q6_Ww_vmpyacc_WwVhRh(sum, samples, weight); } *(HVX_Vector *)(row + x) = Q6_Vh_vasr_VwVwR_rnd_sat(Q6_V_hi_W(sum), Q6_V_lo_W(sum), shift); } for (; x < width; x++) { int sum = 0; for (int k = 0; k < filter->tap_count; k++) { const uint8_t *line = src + filter->indices[k] * stride; int sample = format == CDSP_VPP_P010 ? ((const uint16_t *)line)[x] >> 6 : line[x] << 2; sum += sample * filter->weights[k]; } row[x] = (sum + (1 << (shift - 1))) >> shift; } } static void wide_horizontal(ScalerState *state, int plane, uint8_t *dst, const int16_t *row, int hvx) { int shift = state->horizontal_filters[plane][0].shift; int extra = 4 + (state->dst_format == CDSP_VPP_NV12 ? 2 : 0); if (hvx) { for (int t = 0; t < state->wide_tile_count[plane]; t++) { WideTile *tile = &state->wide_tiles[plane][t]; HVX_Vector lo = Q6_Vb_vshuff_Vb(*(const UnalignedVector *)(row + tile->src_base)); HVX_Vector hi = Q6_V_vzero(); if (tile->spans_two_vectors) hi = Q6_Vb_vshuff_Vb(*(const UnalignedVector *)(row + tile->src_base + 64)); HVX_VectorPair sum = Q6_W_vcombine_VV(Q6_V_vzero(), Q6_V_vzero()); for (int k = 0; k < tile->tap_count; k++) { HVX_Vector samples = gather_bytes(*(HVX_Vector *)tile->indices[k], lo, hi, tile->spans_two_vectors); sum = Q6_Ww_vmpyacc_WwVhVh(sum, samples, *(HVX_Vector *)tile->weights[k]); } // The narrowing shift is four bits wide; discard fractional bits // separately without rounding, then round exactly once at output. HVX_Vector values = Q6_Vh_vasr_VwVwR_rnd_sat(Q6_Vw_vasr_VwR(Q6_V_hi_W(sum), extra), Q6_Vw_vasr_VwR(Q6_V_lo_W(sum), extra), shift); int bytes = tile->components; HVX_Vector out; if (state->dst_format == CDSP_VPP_P010) { values = Q6_Vh_vmax_VhVh(values, Q6_V_vzero()); values = Q6_Vh_vmin_VhVh(values, Q6_V_vsplat_R(0x03ff03ff)); out = Q6_Vh_vasl_VhR(values, 6); bytes *= 2; } else out = Q6_Vub_vpack_VhVh_sat(Q6_V_vzero(), values); uint8_t *target = dst + tile->dst_offset * (state->dst_format == CDSP_VPP_P010 ? 2 : 1); if (bytes == 128) *(UnalignedVector *)target = out; else { uint8_t tail[128] __attribute__((aligned(128))); *(HVX_Vector *)tail = out; memcpy(target, tail, bytes); } } return; } int channels = plane ? 2 : 1; for (int x = 0; x < state->dst_width; x++) { Filter *filter = &state->horizontal_filters[plane][x / channels]; int sum = 0; for (int k = 0; k < filter->tap_count; k++) sum += row[filter->indices[k] * channels + x % channels] * filter->weights[k]; int value = (sum + (1 << (shift + extra - 1))) >> (shift + extra); if (state->dst_format == CDSP_VPP_P010) ((uint16_t *)dst)[x] = clamp_int(value, 0, 1023) << 6; else dst[x] = clamp_int(value, 0, 255); } } static HVX_VectorPair load_10bit_samples(const uint8_t *src, int format) { HVX_Vector lo, hi; if (format == CDSP_VPP_P010) { lo = Q6_Vuh_vlsr_VuhR(*(const UnalignedVector *)src, 6); hi = Q6_Vuh_vlsr_VuhR(*(const UnalignedVector *)(src + 128), 6); } else { HVX_VectorPair words = Q6_Wuh_vunpack_Vub(*(const UnalignedVector *)src); lo = Q6_Vh_vasl_VhR(Q6_V_lo_W(words), 2); hi = Q6_Vh_vasl_VhR(Q6_V_hi_W(words), 2); } return Q6_W_vcombine_VV(hi, lo); } static void wide_downscale_2x_row(ScalerState *state, const uint8_t *r0, const uint8_t *r1, uint8_t *dst, int channels, int hvx) { int x = 0, src_bytes = state->src_format == CDSP_VPP_P010 ? 2 : 1; if (hvx) for (; x + 64 <= state->dst_width; x += 64) { HVX_VectorPair a = load_10bit_samples(r0 + 2 * x * src_bytes, state->src_format); HVX_VectorPair b = load_10bit_samples(r1 + 2 * x * src_bytes, state->src_format); HVX_Vector lo = Q6_Vh_vadd_VhVh(Q6_V_lo_W(a), Q6_V_lo_W(b)); HVX_Vector hi = Q6_Vh_vadd_VhVh(Q6_V_hi_W(a), Q6_V_hi_W(b)); // Deinterleave whole U/V pairs for chroma, individual words for Y. HVX_VectorPair pairs = Q6_W_vdeal_VVR(hi, lo, channels == 2 ? -4 : -2); HVX_Vector sum = Q6_Vh_vadd_VhVh(Q6_V_lo_W(pairs), Q6_V_hi_W(pairs)); if (state->dst_format == CDSP_VPP_P010) { sum = Q6_Vuh_vlsr_VuhR(Q6_Vh_vadd_VhVh(sum, Q6_V_vsplat_R(0x00020002)), 2); *(UnalignedVector *)(dst + 2 * x) = Q6_Vh_vasl_VhR(sum, 6); } else { sum = Q6_Vuh_vlsr_VuhR(Q6_Vh_vadd_VhVh(sum, Q6_V_vsplat_R(0x00080008)), 4); HVX_Vector out = Q6_Vub_vpack_VhVh_sat(Q6_V_vzero(), sum); store_partial_vector(dst + x, out, 64); } } for (; x < state->dst_width; x++) { int index = 2 * (x / channels) * channels + x % channels; int sum; if (state->src_format == CDSP_VPP_P010) { const uint16_t *a = (const uint16_t *)r0, *b = (const uint16_t *)r1; sum = (a[index] >> 6) + (a[index + channels] >> 6) + (b[index] >> 6) + (b[index + channels] >> 6); } else sum = (r0[index] + r0[index + channels] + r1[index] + r1[index + channels]) * 4; if (state->dst_format == CDSP_VPP_P010) ((uint16_t *)dst)[x] = ((sum + 2) >> 2) << 6; else dst[x] = clamp_int((sum + 8) >> 4, 0, 255); } } static void process_rows(ScalerState *state, const uint8_t *src, uint8_t *dst, int src_stride, int dst_stride, int plane, int hvx, int worker, int first_row, int end_row) { if (state->src_format == CDSP_VPP_NV12 && state->dst_format == CDSP_VPP_NV12) { scale_plane(state, src, dst, src_stride, dst_stride, plane, hvx, worker, first_row, end_row); return; } int height = state->dst_height / (plane ? 2 : 1); int same_size = state->src_width == state->dst_width && state->src_height == state->dst_height; int halve = state->src_width == 2 * state->dst_width && state->src_height == 2 * state->dst_height; int16_t *row = (same_size || halve) ? 0 : (int16_t *)((uint8_t *)state->wide_rows + worker * cubic_row_stride_bytes(state->src_width) + 128); for (int y = first_row; y < end_row; y++) { if (same_size) { convert_row(src + y * src_stride, dst + (y - first_row) * dst_stride, state->dst_width, state->src_format, state->dst_format, hvx); continue; } if (hvx && src_stride <= 65535) { int ahead = y + 2 < height ? y + 2 : y; Filter *filter = &state->vertical_filters[plane][ahead]; int bytes = state->src_width * (state->src_format == CDSP_VPP_P010 ? 2 : 1); Q6_l2fetch_AP((void *)(src + filter->indices[0] * src_stride), ((uint64_t)src_stride << 32) | ((uint64_t)bytes << 16) | (filter->indices[filter->tap_count - 1] - filter->indices[0] + 1)); } if (halve) { wide_downscale_2x_row(state, src + 2 * y * src_stride, src + (2 * y + 1) * src_stride, dst + (y - first_row) * dst_stride, plane ? 2 : 1, hvx); continue; } uint64_t start = state->profiling ? HAP_perf_get_time_us() : 0; if (hvx && state->src_format == CDSP_VPP_NV12 && state->dst_height == 2 * state->src_height) cubic_vertical_2x(src, src_stride, row, state->src_width, &state->vertical_filters[plane][y]); else if (state->src_format == CDSP_VPP_NV12 && state->cubic_rows) cubic_vertical(src, src_stride, row, state->src_width, &state->vertical_filters[plane][y], hvx); else wide_vertical(src, src_stride, row, state->src_width, &state->vertical_filters[plane][y], state->src_format, hvx); uint64_t middle = state->profiling ? HAP_perf_get_time_us() : 0; if (hvx && state->cubic_rows) cubic_horizontal(state, plane, dst + (y - first_row) * dst_stride, row); else wide_horizontal(state, plane, dst + (y - first_row) * dst_stride, row, hvx); if (state->profiling) { state->profile_us[worker * 4 + plane * 2] += middle - start; state->profile_us[worker * 4 + plane * 2 + 1] += HAP_perf_get_time_us() - middle; } } } // Reverse pixels, treating each interleaved U/V pair as one indivisible unit. static void reverse_row(const uint8_t *src, uint8_t *dst, int pixels, int pixel_bytes, int hvx) { int bytes = pixels * pixel_bytes, x = 0; if (hvx) { HVX_Vector control = Q6_V_vsplat_R((128 - pixel_bytes) * 0x01010101u); for (; x + 128 <= bytes; x += 128) { HVX_Vector v = *(const UnalignedVector *)(src + bytes - x - 128); *(UnalignedVector *)(dst + x) = Q6_V_vdelta_VV(v, control); } } for (; x < bytes; x += pixel_bytes) memcpy(dst + x, src + bytes - x - pixel_bytes, pixel_bytes); } // A square tile has 128 / pixel_bytes rows and columns. Butterfly stages // transpose whole pixels (including U/V pairs), so each complete output row // becomes one 128-byte store. The two work arrays live outside the worker stack. static void rotate_strip(const uint8_t *restrict src, int src_stride, uint8_t *restrict dst, int dst_stride, int width, int height, int first_row, int rows, int pixel_bytes, int rotation, int hvx, uint8_t *restrict workspace, int tile_rows) { if (rotation == CDSP_VPP_ROTATE_180) { for (int y = 0; y < rows; y++) reverse_row(src + y * src_stride, dst + (height - first_row - y - 1) * dst_stride, width, pixel_bytes, hvx); return; } int clockwise = rotation == CDSP_VPP_ROTATE_90; int column = clockwise ? height - first_row - rows : first_row; if (hvx) { HVX_Vector(*buffers)[ROTATION_MAX_ROWS] = (HVX_Vector(*)[ROTATION_MAX_ROWS])workspace; int columns = 128 / pixel_bytes; for (int x = 0; x < width; x += columns) { int count = width - x < columns ? width - x : columns; for (int y = 0; y < tile_rows; y++) { buffers[0][y] = Q6_V_vzero(); if (y < rows) { const uint8_t *line = src + (clockwise ? rows - y - 1 : y) * src_stride + x * pixel_bytes; if (count == columns) buffers[0][y] = *(const UnalignedVector *)line; else memcpy(&buffers[0][y], line, count * pixel_bytes); } } int stage = 0; for (int group = 1; group < tile_rows; stage++, group *= 2) { HVX_Vector *in = buffers[stage & 1], *out = buffers[(stage + 1) & 1]; for (int base = 0; base < tile_rows; base += 2 * group) for (int i = 0; i < group; i++) { HVX_VectorPair pair = Q6_W_vshuff_VVR(in[base + i + group], in[base + i], -pixel_bytes * group); out[base + 2 * i] = Q6_V_lo_W(pair); out[base + 2 * i + 1] = Q6_V_hi_W(pair); } } const uint8_t *transposed = (const uint8_t *)buffers[stage & 1]; for (int i = 0; i < count; i++) { int output_row = clockwise ? x + i : width - x - i - 1; uint8_t *target = dst + output_row * dst_stride + column * pixel_bytes; HVX_Vector values = ((const HVX_Vector *)transposed)[i]; if (rows == tile_rows) *(UnalignedVector *)target = values; else store_partial_vector(target, values, rows * pixel_bytes); } } return; } for (int x = 0; x < width; x++) for (int y = 0; y < rows; y++) { int output_row = clockwise ? x : width - x - 1; int output_column = clockwise ? height - first_row - y - 1 : first_row + y; memcpy(dst + output_row * dst_stride + output_column * pixel_bytes, src + y * src_stride + x * pixel_bytes, pixel_bytes); } } static void process_plane(ScalerState *state, const uint8_t *src, uint8_t *dst, int src_stride, int dst_stride, int plane, int hvx, int worker, int workers) { int channels = plane ? 2 : 1; int height = state->dst_height / channels; if (!state->rotation) { int first = height * worker / workers, end = height * (worker + 1) / workers; process_rows(state, src, dst + first * dst_stride, src_stride, dst_stride, plane, hvx, worker, first, end); return; } int pixel_bytes = channels * (state->dst_format == CDSP_VPP_P010 ? 2 : 1); int direct = state->src_format == state->dst_format && state->src_width == state->dst_width && state->src_height == state->dst_height; int tile_rows = hvx && (state->rotation & 1) ? 128 / pixel_bytes : 16; int strips = (height + tile_rows - 1) / tile_rows; for (int block = strips * worker / workers; block < strips * (worker + 1) / workers; block++) { int first = block * tile_rows; int rows = height - first < tile_rows ? height - first : tile_rows; const uint8_t *input = src; int stride = src_stride; if (!direct) { uint8_t *scratch = state->rotation_rows.data + worker * state->rotation_worker_rows * state->rotation_stride; stride = state->rotation_stride; process_rows(state, src, scratch, src_stride, stride, plane, hvx, worker, first, first + rows); input = scratch; } else input += first * src_stride; rotate_strip(input, stride, dst, dst_stride, state->dst_width / channels, height, first, rows, pixel_bytes, state->rotation, hvx, hvx && (state->rotation & 1) ? state->transpose_workspace.data + worker * TRANSPOSE_WORKER_BYTES : 0, tile_rows); } } static void worker_main(void *opaque) { Worker *worker = opaque; ScalerState *state = worker->state; for (;;) { qurt_sem_down(&worker->work); if (state->stop) break; worker->status = qurt_hvx_lock(QURT_HVX_MODE_128B); if (!worker->status) { if (state->overlay_job) overlay_rows(&state->overlay, state->src, state->rgba, state->dst, worker->worker_id, state->worker_count, 1); else { process_plane(state, state->src + state->src_y_offset, state->dst + state->dst_y_offset, state->src_stride, state->dst_stride, 0, 1, worker->worker_id, state->worker_count); process_plane(state, state->src + state->src_uv_offset, state->dst + state->dst_uv_offset, state->src_stride, state->dst_stride, 1, 1, worker->worker_id, state->worker_count); } // Publish HVX stores before the RPC thread observes completion. qurt_mem_syncht(); qurt_hvx_unlock(); } qurt_sem_up(&worker->done); } qurt_thread_exit(0); } static int plane_fits_buffer(int buffer_size, int offset, int stride, int width, int height) { return offset >= 0 && stride >= width && (uint64_t)offset + (uint64_t)(height - 1) * stride + width <= (uint64_t)buffer_size; } int iris_vpp_run(remote_handle64 h, const uint8_t *src, int src_size, uint8_t *dst, int dst_size, int src_stride, int dst_stride, int src_y_offset, int src_uv_offset, int dst_y_offset, int dst_uv_offset, int worker_count, uint64_t *us) { return iris_vpp_process(h, src, src_size, dst, dst_size, src_stride, dst_stride, src_y_offset, src_uv_offset, dst_y_offset, dst_uv_offset, worker_count, CDSP_VPP_NV12, CDSP_VPP_NV12, us); } int iris_vpp_process(remote_handle64 h, const uint8_t *src, int src_size, uint8_t *dst, int dst_size, int src_stride, int dst_stride, int src_y_offset, int src_uv_offset, int dst_y_offset, int dst_uv_offset, int worker_count, int src_format, int dst_format, uint64_t *us) { return iris_vpp_transform(h, src, src_size, dst, dst_size, src_stride, dst_stride, src_y_offset, src_uv_offset, dst_y_offset, dst_uv_offset, worker_count, src_format, dst_format, CDSP_VPP_ROTATE_NONE, us); } int iris_vpp_transform(remote_handle64 h, const uint8_t *src, int src_size, uint8_t *dst, int dst_size, int src_stride, int dst_stride, int src_y_offset, int src_uv_offset, int dst_y_offset, int dst_uv_offset, int worker_count, int src_format, int dst_format, int rotation, uint64_t *us) { if (rotation < CDSP_VPP_ROTATE_NONE || rotation > CDSP_VPP_ROTATE_270) return AEE_EBADPARM; ScalerState *state = (ScalerState *)(uintptr_t)h; if ((src_format != CDSP_VPP_NV12 && src_format != CDSP_VPP_P010) || (dst_format != CDSP_VPP_NV12 && dst_format != CDSP_VPP_P010)) return AEE_EBADPARM; int src_bytes = src_format == CDSP_VPP_P010 ? 2 : 1; int dst_bytes = dst_format == CDSP_VPP_P010 ? 2 : 1; int output_width = (rotation & 1) ? state->dst_height : state->dst_width; int output_height = (rotation & 1) ? state->dst_width : state->dst_height; if (((src_stride | src_y_offset | src_uv_offset) & (src_bytes - 1)) || ((dst_stride | dst_y_offset | dst_uv_offset) & (dst_bytes - 1))) return AEE_EBADPARM; if (!state->src_width || !src || !dst || src_size < 0 || dst_size < 0 || worker_count < 0 || worker_count > CDSP_VPP_MAX_WORKERS || !plane_fits_buffer(src_size, src_y_offset, src_stride, state->src_width * src_bytes, state->src_height) || !plane_fits_buffer(src_size, src_uv_offset, src_stride, state->src_width * src_bytes, state->src_height / 2) || !plane_fits_buffer(dst_size, dst_y_offset, dst_stride, output_width * dst_bytes, output_height) || !plane_fits_buffer(dst_size, dst_uv_offset, dst_stride, output_width * dst_bytes, output_height / 2)) return AEE_EBADPARM; state->overlay_job = 0; state->rotation = rotation; state->src_format = src_format; state->dst_format = dst_format; if ((src_format == CDSP_VPP_P010 || dst_format == CDSP_VPP_P010) && (state->src_width != state->dst_width || state->src_height != state->dst_height) && !(state->src_width == 2 * state->dst_width && state->src_height == 2 * state->dst_height)) { int e = prepare_wide_filters(state); if (e) return e; } int transpose = worker_count && (rotation & 1); int workers = worker_count ? worker_count : 1; int e = resize_rotation_buffer(&state->transpose_workspace, transpose ? workers * TRANSPOSE_WORKER_BYTES : 0, transpose); if (e) return e; // Conversion/scaling feeds bounded strips, never a second full frame. // Pure same-format rotation can read the mapped source directly. state->rotation_worker_rows = transpose ? 128 / dst_bytes : 16; state->rotation_stride = (state->dst_width * dst_bytes + 127) & ~127; int needs_strip = rotation && (src_format != dst_format || state->src_width != state->dst_width || state->src_height != state->dst_height); unsigned strip_bytes = needs_strip ? workers * state->rotation_worker_rows * state->rotation_stride : 0; e = resize_rotation_buffer(&state->rotation_rows, strip_bytes, transpose); if (e) return e; uint64_t start_us = HAP_perf_get_time_us(); if (state->profiling) memset(state->profile_us, 0, sizeof(state->profile_us)); if (worker_count > 1) { state->src = src; state->dst = dst; state->src_stride = src_stride; state->dst_stride = dst_stride; state->src_y_offset = src_y_offset; state->src_uv_offset = src_uv_offset; state->dst_y_offset = dst_y_offset; state->dst_uv_offset = dst_uv_offset; state->worker_count = worker_count; for (int i = 0; i < worker_count; i++) qurt_sem_up(&state->workers[i].work); int status = 0; for (int i = 0; i < worker_count; i++) { qurt_sem_down(&state->workers[i].done); if (state->workers[i].status) status = AEE_EFAILED; } *us = HAP_perf_get_time_us() - start_us; return status; } if (worker_count && qurt_hvx_lock(QURT_HVX_MODE_128B) != 0) return AEE_EFAILED; process_plane(state, src + src_y_offset, dst + dst_y_offset, src_stride, dst_stride, 0, worker_count, 0, 1); process_plane(state, src + src_uv_offset, dst + dst_uv_offset, src_stride, dst_stride, 1, worker_count, 0, 1); if (worker_count) qurt_hvx_unlock(); *us = HAP_perf_get_time_us() - start_us; return 0; } int iris_vpp_overlay(remote_handle64 h, const uint8_t *src, int src_size, const uint8_t *rgba, int rgba_size, uint8_t *dst, int dst_size, const uint8_t *params, int params_size, int worker_count, uint64_t *us) { if (!h || !src || !rgba || !dst || !params || !us || src_size < 0 || rgba_size < 0 || dst_size < 0 || params_size != sizeof(OverlayParams) || worker_count < 0 || worker_count > CDSP_VPP_MAX_WORKERS) return AEE_EBADPARM; ScalerState *state = (ScalerState *)(uintptr_t)h; memcpy(&state->overlay, params, sizeof(state->overlay)); if (!overlay_valid(&state->overlay, src_size, rgba_size, dst_size)) return AEE_EBADPARM; state->overlay_job = 1; state->src = src; state->rgba = rgba; state->dst = dst; state->worker_count = worker_count; uint64_t start = HAP_perf_get_time_us(); int status = 0; if (worker_count > 1) { for (int i = 0; i < worker_count; ++i) qurt_sem_up(&state->workers[i].work); for (int i = 0; i < worker_count; ++i) { qurt_sem_down(&state->workers[i].done); if (state->workers[i].status) status = AEE_EFAILED; } } else { if (worker_count && qurt_hvx_lock(QURT_HVX_MODE_128B)) return AEE_EFAILED; overlay_rows(&state->overlay, src, rgba, dst, 0, 1, worker_count); qurt_mem_syncht(); if (worker_count) qurt_hvx_unlock(); } *us = HAP_perf_get_time_us() - start; return status; } qcom-va-driver-1.1.0/src/cdsp/filter.h000066400000000000000000000062001526764642600175430ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef VPP_FILTER_H #define VPP_FILTER_H #include #include #include // At most 20:1 reduction, plus room for partially covered endpoint samples. #define FILTER_MAX_TAPS 22 // Q7 area reduction and Q14 Catmull-Rom enlargement. Weights sum to 1< maximum ? maximum : x; } static inline double catmull_rom_weight(double x) { x = fabs(x); if (x < 1) return (1.5 * x - 2.5) * x * x + 1; if (x < 2) return ((-.5 * x + 2.5) * x - 4) * x + 2; return 0; } static inline Filter make_filter(int src_size, int dst_size, int x) { Filter filter; memset(&filter, 0, sizeof(filter)); double weights[FILTER_MAX_TAPS]; int sum = 0; filter.shift = src_size >= dst_size ? 7 : 14; if (src_size > dst_size) { // Area weights are source-pixel overlap with this destination footprint. double left = (double)x * src_size / dst_size, right = (double)(x + 1) * src_size / dst_size; int first = (int)floor(left), last = (int)ceil(right); for (int i = first; i < last; i++) { double overlap_start = left > i ? left : i, overlap_end = right < i + 1 ? right : i + 1; filter.indices[filter.tap_count] = clamp_int(i, 0, src_size - 1); weights[filter.tap_count++] = (overlap_end - overlap_start) / (right - left); } } else if (src_size == dst_size) { filter.tap_count = 1; filter.indices[0] = x; weights[0] = 1; } else { // Align pixel centers. Clamp source indices to replicate the border, // while evaluating the four cubic weights at the unclamped positions. double src_position = ((double)x + .5) * src_size / dst_size - .5; int first = (int)floor(src_position) - 1; for (int i = first; i < first + 4; i++) { filter.indices[filter.tap_count] = clamp_int(i, 0, src_size - 1); weights[filter.tap_count++] = catmull_rom_weight(src_position - i); } } for (int k = 0; k < filter.tap_count; k++) { filter.weights[k] = (int16_t)floor(weights[k] * (1 << filter.shift) + .5); sum += filter.weights[k]; } // Correct quantization residuals so a constant input stays constant. Adjust // the tap with the largest rounding error in the required direction first. while (sum != (1 << filter.shift)) { int direction = sum < (1 << filter.shift) ? 1 : -1, best = 0; for (int k = 1; k < filter.tap_count; k++) if (direction * (weights[k] * (1 << filter.shift) - filter.weights[k]) > direction * (weights[best] * (1 << filter.shift) - filter.weights[best])) best = k; filter.weights[best] += direction; sum += direction; } return filter; } static inline uint8_t round_clip_u8(int x, int shift) { return (uint8_t)clamp_int((x + (1 << (shift - 1))) >> shift, 0, 255); } #endif qcom-va-driver-1.1.0/src/cdsp/iris_vpp.idl000066400000000000000000000027531526764642600204430ustar00rootroot00000000000000#include "remote.idl" interface iris_vpp : remote_handle64 { long power(rout long mask); long configure(in long src_width, in long src_height, in long dst_width, in long dst_height); long run(in sequence src, inrout sequence dst, in long src_stride, in long dst_stride, in long src_y_offset, in long src_uv_offset, in long dst_y_offset, in long dst_uv_offset, in long worker_count, rout unsigned long long usec); long info(rout unsigned long hz, rout long hvx_units); long profile(in long enable, rout sequence counters); long process(in sequence src, inrout sequence dst, in long src_stride, in long dst_stride, in long src_y_offset, in long src_uv_offset, in long dst_y_offset, in long dst_uv_offset, in long worker_count, in long src_format, in long dst_format, rout unsigned long long usec); long transform(in sequence src, inrout sequence dst, in long src_stride, in long dst_stride, in long src_y_offset, in long src_uv_offset, in long dst_y_offset, in long dst_uv_offset, in long worker_count, in long src_format, in long dst_format, in long rotation, rout unsigned long long usec); long overlay(in sequence base, in sequence rgba, inrout sequence dst, in sequence params, in long worker_count, rout unsigned long long usec); }; qcom-va-driver-1.1.0/src/cdsp/meson.build000066400000000000000000000075531526764642600202630ustar00rootroot00000000000000# The supplied Open Access package executes on Linux ARM64, independently of # the host C/C++ compiler used to build the VA driver and FastRPC client. if build_machine.system() != 'linux' or build_machine.cpu_family() != 'aarch64' error('cdsp_vpp requires a Linux ARM64 build machine (Open Access 19.0.02)') endif python = import('python').find_installation() qaic_project = subproject('qaic') qaic = qaic_project.get_variable('qaic') qaic_idl_dir = qaic_project.get_variable('qaic_idl_dir') hexagon = subproject('hexagon').get_variable('hexagon_tools') cc = meson.get_compiler('c') # Some distributions ship libcdsprpc without a pkg-config file. fastrpc_include_dir = get_option('fastrpc_include_dir') fastrpc_mode = get_option('fastrpc') fastrpc_dep = dependency('', required: false) if fastrpc_mode != 'bundled' and import('fs').is_dir(fastrpc_include_dir) rpc_library = cc.find_library('cdsprpc', required: false) rpc_includes = include_directories(fastrpc_include_dir, is_system: true) if rpc_library.found() and cc.links(''' #include #include int main(void) { fastrpc_capability capability = {CDSP_DOMAIN_ID, ARCH_VER, 0}; remote_handle_control(DSPRPC_GET_DSP_INFO, &capability, sizeof(capability)); capability.attribute_ID = HVX_SUPPORT_128B; remote_handle_control(DSPRPC_GET_DSP_INFO, &capability, sizeof(capability)); remote_register_buf(0, 0, -1); fastrpc_mmap(CDSP_DOMAIN_ID, -1, 0, 0, 0, FASTRPC_MAP_STATIC); fastrpc_munmap(CDSP_DOMAIN_ID, -1, 0, 0); return remote_session_control(0, 0, 0); }''', dependencies: rpc_library, include_directories: rpc_includes, name: 'FastRPC mapping and architecture query APIs') fastrpc_dep = declare_dependency(dependencies: rpc_library, include_directories: rpc_includes) endif endif if not fastrpc_dep.found() if fastrpc_mode == 'system' error('Compatible system FastRPC headers and libcdsprpc are required') endif rpc_project = subproject('fastrpc') fastrpc_dep = rpc_project.get_variable('fastrpc_dep') fastrpc_include_dir = rpc_project.get_variable('fastrpc_include_dir') summary('FastRPC', 'pinned source fallback') else summary('FastRPC', 'system') endif rpc_generated = custom_target('iris-vpp-rpc', input: 'iris_vpp.idl', output: ['iris_vpp.h', 'iris_vpp_stub.c', 'iris_vpp_skel.c'], command: [qaic, '-mdll', '-st', '-I', qaic_idl_dir, '-o', '@OUTDIR@', '@INPUT@']) foreach arch : ['v68', 'v69', 'v73', 'v75', 'v79', 'v81'] custom_target('iris-vpp-dsp-' + arch, input: rpc_generated, output: 'vpp_' + arch + '_skel.so', command: [python, files('build-dsp.py'), arch, hexagon, fastrpc_include_dir, meson.current_build_dir(), meson.current_source_dir(), '@OUTPUT@'], depend_files: files('dsp.c', 'overlay.h', '../overlay-params.h', 'filter.h', 'qurt_abi.h', 'qurt_abi_checks.h', 'vpp_limits.h'), build_by_default: true, install: true, install_dir: get_option('cdsp_firmware_dir')) endforeach skel_args = ['-DIRIS_VPP_DSP_DIR="' + (get_option('prefix') / get_option('cdsp_firmware_dir')) + '"'] # Keep FastRPC out of the VA driver's DT_NEEDED list: decoding and encoding # remain available when the optional VPP runtime cannot be loaded. vpp_client = shared_library('iris_vpp', 'client.cpp', 'skel.cpp', rpc_generated[0], rpc_generated[1], dependencies: [fastrpc_dep, threads], c_args: ['-DSTATIC_LIB', '-Wno-unused-parameter', '-Wno-type-limits'], cpp_args: skel_args, install: true, install_dir: get_option('libdir') / 'qcom-va-driver', install_rpath: '$ORIGIN') # Mock only the FastRPC boundary; exercise the production selector without a DSP. skel_test = executable('vpp-skel-selection', '../../tests/vpp/skel-selection.cpp', 'skel.cpp', rpc_generated[0], dependencies: fastrpc_dep.partial_dependency(includes: true), include_directories: include_directories('.'), cpp_args: skel_args, build_by_default: false) test('CDSP skeleton selection', skel_test) qcom-va-driver-1.1.0/src/cdsp/overlay.h000066400000000000000000000404651526764642600177520ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef CDSP_OVERLAY_H #define CDSP_OVERLAY_H #include "../overlay-params.h" // Premultiply before interpolation: invisible RGB must not darken scaled edges. typedef struct { int r, g, b, a; } OverlayPixel; static inline int overlay_clamp(int x, int lo, int hi) { return x < lo ? lo : x > hi ? hi : x; } static inline OverlayPixel overlay_pixel(const OverlayParams *p, const uint8_t *rgb, int x, int y) { const uint8_t *v = rgb + p->rgb_offset + y * p->rgb_stride + x * 4; int a = v[3], factor = p->flags & OVERLAY_PREMULTIPLIED ? 255 : a; OverlayPixel result = {v[p->flags & OVERLAY_BGRA ? 2 : 0] * factor, v[1] * factor, v[p->flags & OVERLAY_BGRA ? 0 : 2] * factor, a}; return result; } static inline OverlayPixel overlay_lerp(OverlayPixel a, OverlayPixel b, int weight) { OverlayPixel result = {(a.r * (256 - weight) + b.r * weight + 128) >> 8, (a.g * (256 - weight) + b.g * weight + 128) >> 8, (a.b * (256 - weight) + b.b * weight + 128) >> 8, (a.a * (256 - weight) + b.a * weight + 128) >> 8}; return result; } static inline OverlayPixel overlay_sample(const OverlayParams *p, const uint8_t *rgb, int x, int y) { OverlayPixel v = {0, 0, 0, 0}; x -= p->x; y -= p->y; if (!p->alpha || x < 0 || y < 0 || x >= p->w || y >= p->h) return v; if (p->w == p->rgb_width && p->h == p->rgb_height) v = overlay_pixel(p, rgb, x, y); else { int sx = overlay_clamp(x * p->step_x + p->step_x / 2 - 32768, 0, (p->rgb_width - 1) << 16); int sy = overlay_clamp(y * p->step_y + p->step_y / 2 - 32768, 0, (p->rgb_height - 1) << 16); int x0 = sx >> 16, y0 = sy >> 16; int x1 = overlay_clamp(x0 + 1, 0, p->rgb_width - 1), y1 = overlay_clamp(y0 + 1, 0, p->rgb_height - 1); v = overlay_lerp(overlay_lerp(overlay_pixel(p, rgb, x0, y0), overlay_pixel(p, rgb, x1, y0), (sx >> 8) & 255), overlay_lerp(overlay_pixel(p, rgb, x0, y1), overlay_pixel(p, rgb, x1, y1), (sx >> 8) & 255), (sy >> 8) & 255); } v.r = (v.r * p->alpha + 128) >> 8; v.g = (v.g * p->alpha + 128) >> 8; v.b = (v.b * p->alpha + 128) >> 8; v.a = (v.a * p->alpha + 128) >> 8; return v; } static inline int overlay_dot(const int32_t *c, OverlayPixel v) { return (c[0] * v.r + c[1] * v.g + c[2] * v.b + 8192) >> 14; } static inline int overlay_load(const uint8_t *src, int offset, int wide) { return wide ? *(const uint16_t *)(src + offset) >> 6 : src[offset]; } static inline void overlay_store(uint8_t *dst, int offset, int value, int wide) { value = overlay_clamp(value, 0, wide ? 1023 : 255); if (wide) *(uint16_t *)(dst + offset) = value << 6; else dst[offset] = value; } static inline void overlay_block(const OverlayParams *p, const uint8_t *src, const uint8_t *rgb, uint8_t *dst, int x, int y) { int wide = !!(p->flags & OVERLAY_P010), bytes = 1 + wide, scale = wide ? 4 : 1; int sum_a = 0, sum_u = 0, sum_v = 0; for (int row = 0; row < 2; ++row) for (int col = 0; col < 2; ++col) { OverlayPixel v = overlay_sample(p, rgb, x + col, y + row); int base = overlay_load(src, p->src_y + (y + row) * p->src_stride + (x + col) * bytes, wide); int value = (base * (255 - v.a) + p->black * scale * v.a + overlay_dot(p->matrix, v) * scale + 127) / 255; overlay_store(dst, p->dst_y + (y + row) * p->dst_stride + (x + col) * bytes, value, wide); sum_a += v.a; sum_u += overlay_dot(p->matrix + 3, v); sum_v += overlay_dot(p->matrix + 6, v); } // One owner per 2x2 block. Average premultiplied chroma and coverage together, // including uncovered pixels at odd/clipped rectangle edges. for (int c = 0; c < 2; ++c) { int base = overlay_load(src, p->src_uv + (y / 2) * p->src_stride + (x + c) * bytes, wide); int value = (base * (1020 - sum_a) + 128 * scale * sum_a + (c ? sum_v : sum_u) * scale + 510) / 1020; overlay_store(dst, p->dst_uv + (y / 2) * p->dst_stride + (x + c) * bytes, value, wide); } } // Thirty-two RGBA pixels occupy one HVX register. Keep color and coverage in // 32-bit lanes until the final pack; this also preserves P010 rounding. typedef int32_t OverlayWords __attribute__((ext_vector_type(32))); static inline OverlayWords overlay_div255(OverlayWords n) { // Exact for the nonnegative blend numerators (at most 1020*1023+510). OverlayWords q = (n + (n >> 8) + (n >> 16)) >> 8; return q + ((n - q * 255 > 254) & 1); } static inline OverlayWords overlay_mul(OverlayWords a, OverlayWords b) { return (OverlayWords)Q6_Vw_vmpyie_VwVuh((HVX_Vector)a, (HVX_Vector)b); } static inline OverlayWords overlay_dot_vector(const int32_t *c, OverlayWords r, OverlayWords g, OverlayWords b) { HVX_Vector v = Q6_Vw_vmpyi_VwRh((HVX_Vector)r, (uint16_t)c[0] * 0x10001u); v = Q6_Vw_vmpyiacc_VwVwRh(v, (HVX_Vector)g, (uint16_t)c[1] * 0x10001u); v = Q6_Vw_vmpyiacc_VwVwRh(v, (HVX_Vector)b, (uint16_t)c[2] * 0x10001u); return ((OverlayWords)v + 8192) >> 14; } static inline OverlayWords overlay_load_vector(const uint8_t *src, int wide, int remaining) { HVX_Vector raw; if (remaining >= 128) raw = *(const UnalignedVector *)src; else { raw = Q6_V_vzero(); if (wide) __builtin_memcpy_inline(&raw, src, 64); else __builtin_memcpy_inline(&raw, src, 32); } if (!wide) raw = Q6_V_lo_W(Q6_Wuh_vunpack_Vub(raw)); OverlayWords v = (OverlayWords)Q6_V_lo_W(Q6_Wuw_vunpack_Vuh(raw)); return wide ? v >> 6 : v; } static inline void overlay_store_vector(uint8_t *dst, OverlayWords v, int wide) { v = v < 0 ? 0 : v; int max = wide ? 1023 : 255; v = v > max ? max : v; if (wide) v <<= 6; HVX_Vector packed = Q6_Vh_vpacke_VwVw((HVX_Vector)v, (HVX_Vector)v); if (!wide) packed = Q6_Vb_vpacke_VhVh(packed, packed); store_partial_vector(dst, packed, 32 * (wide ? 2 : 1)); } static inline OverlayWords overlay_pairs(OverlayWords v) { return v + (OverlayWords)Q6_V_vror_VR((HVX_Vector)v, 4); } static inline void overlay_copy(uint8_t *dst, const uint8_t *src, int count, int hvx) { if (src == dst || !count) return; if (hvx) { for (; count >= 128; count -= 128, src += 128, dst += 128) *(UnalignedVector *)dst = *(const UnalignedVector *)src; } for (; count >= 8; count -= 8, src += 8, dst += 8) { uint64_t word; __builtin_memcpy_inline(&word, src, 8); __builtin_memcpy_inline(dst, &word, 8); } for (int i = 0; i < count; ++i) dst[i] = src[i]; } typedef struct { OverlayWords r, g, b, a; } OverlayVectorPixel; static inline OverlayVectorPixel overlay_unpack(const OverlayParams *p, OverlayWords v) { OverlayWords a = (v >> 24) & 255, factor = p->flags & OVERLAY_PREMULTIPLIED ? (OverlayWords)255 : a; OverlayVectorPixel result = { overlay_mul((v >> (p->flags & OVERLAY_BGRA ? 16 : 0)) & 255, factor), overlay_mul((v >> 8) & 255, factor), overlay_mul((v >> (p->flags & OVERLAY_BGRA ? 0 : 16)) & 255, factor), a}; return result; } static inline OverlayVectorPixel overlay_mix(OverlayVectorPixel a, OverlayVectorPixel b, OverlayWords weight) { OverlayWords inverse = 256 - weight; OverlayVectorPixel result = {(overlay_mul(a.r, inverse) + overlay_mul(b.r, weight) + 128) >> 8, (overlay_mul(a.g, inverse) + overlay_mul(b.g, weight) + 128) >> 8, (overlay_mul(a.b, inverse) + overlay_mul(b.b, weight) + 128) >> 8, (overlay_mul(a.a, inverse) + overlay_mul(b.a, weight) + 128) >> 8}; return result; } // A contiguous 32-pixel source window covers the bilinear taps for enlargement. // A byte LUT selects RGBA neighbors without scalar gathers. static inline HVX_Vector overlay_lookup(HVX_Vector indices, HVX_Vector table) { table = Q6_Vb_vshuff_Vb(table); HVX_Vector v = Q6_Vb_vlut32_VbVbI(indices, table, 0); v = Q6_Vb_vlut32or_VbVbVbI(v, indices, table, 1); v = Q6_Vb_vlut32or_VbVbVbI(v, indices, table, 2); return Q6_Vb_vlut32or_VbVbVbI(v, indices, table, 3); } static OverlayVectorPixel overlay_sample_vector(const OverlayParams *p, const uint8_t *rgb, int x, int y) { const OverlayWords lanes = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; OverlayWords pos = (lanes + x - p->x) * p->step_x + p->step_x / 2 - 32768; pos = pos < 0 ? 0 : pos; int max = (p->rgb_width - 1) << 16; pos = pos > max ? max : pos; int first = overlay_clamp((x - p->x) * p->step_x + p->step_x / 2 - 32768, 0, max) >> 16; int table_start = first < p->rgb_width - 32 ? first : p->rgb_width - 32; OverlayWords left = pos >> 16, right = left + 1; right = right > p->rgb_width - 1 ? p->rgb_width - 1 : right; HVX_Vector il = (HVX_Vector)((left - table_start) * 0x04040404 + 0x03020100); HVX_Vector ir = (HVX_Vector)((right - table_start) * 0x04040404 + 0x03020100); int sy = overlay_clamp((y - p->y) * p->step_y + p->step_y / 2 - 32768, 0, (p->rgb_height - 1) << 16); int top = sy >> 16, bottom = top + 1 < p->rgb_height ? top + 1 : top; const uint8_t *row = rgb + p->rgb_offset + table_start * 4; HVX_Vector a = *(const UnalignedVector *)(row + top * p->rgb_stride), b = *(const UnalignedVector *)(row + bottom * p->rgb_stride); OverlayVectorPixel upper = overlay_mix(overlay_unpack(p, (OverlayWords)overlay_lookup(il, a)), overlay_unpack(p, (OverlayWords)overlay_lookup(ir, a)), (pos >> 8) & 255); OverlayVectorPixel lower = overlay_mix(overlay_unpack(p, (OverlayWords)overlay_lookup(il, b)), overlay_unpack(p, (OverlayWords)overlay_lookup(ir, b)), (pos >> 8) & 255); return overlay_mix(upper, lower, (OverlayWords)((sy >> 8) & 255)); } static void overlay_vector(const OverlayParams *p, const uint8_t *src, const uint8_t *rgb, uint8_t *dst, int x, int y) { int wide = !!(p->flags & OVERLAY_P010), bytes = 1 + wide, scale = wide ? 4 : 1; OverlayVectorPixel pixels[2]; int scaled = p->w != p->rgb_width || p->h != p->rgb_height; for (int row = 0; row < 2; ++row) { if (scaled) pixels[row] = overlay_sample_vector(p, rgb, x, y + row); else pixels[row] = overlay_unpack( p, (OverlayWords) * (const UnalignedVector *)(rgb + p->rgb_offset + (y + row - p->y) * p->rgb_stride + (x - p->x) * 4)); } if (!__builtin_reduce_or(pixels[0].a | pixels[1].a)) { for (int row = 0; row < 3; ++row) { const uint8_t *in = src + (row < 2 ? p->src_y + (y + row) * p->src_stride : p->src_uv + (y / 2) * p->src_stride) + x * bytes; uint8_t *out = dst + (row < 2 ? p->dst_y + (y + row) * p->dst_stride : p->dst_uv + (y / 2) * p->dst_stride) + x * bytes; if (in != out) { HVX_Vector value; if ((p->width - x) * bytes >= 128) value = *(const UnalignedVector *)in; else if (wide) __builtin_memcpy_inline(&value, in, 64); else __builtin_memcpy_inline(&value, in, 32); store_partial_vector(out, value, 32 * bytes); } } return; } OverlayWords sum_a = 0, sum_u = 0, sum_v = 0; for (int row = 0; row < 2; ++row) { OverlayWords a = pixels[row].a, r = pixels[row].r, g = pixels[row].g, b = pixels[row].b; a = (overlay_mul(a, (OverlayWords)p->alpha) + 128) >> 8; r = (overlay_mul(r, (OverlayWords)p->alpha) + 128) >> 8; g = (overlay_mul(g, (OverlayWords)p->alpha) + 128) >> 8; b = (overlay_mul(b, (OverlayWords)p->alpha) + 128) >> 8; OverlayWords yy = overlay_dot_vector(p->matrix, r, g, b); sum_u += overlay_dot_vector(p->matrix + 3, r, g, b); sum_v += overlay_dot_vector(p->matrix + 6, r, g, b); sum_a += a; OverlayWords base = overlay_load_vector( src + p->src_y + (y + row) * p->src_stride + x * bytes, wide, (p->width - x) * bytes); OverlayWords value = overlay_div255(base * (255 - a) + p->black * scale * a + yy * scale + 127); overlay_store_vector(dst + p->dst_y + (y + row) * p->dst_stride + x * bytes, value, wide); } OverlayWords u = overlay_pairs(sum_u), v = overlay_pairs(sum_v), a = overlay_pairs(sum_a); OverlayWords uv = __builtin_shufflevector(u, v, 0, 32, 2, 34, 4, 36, 6, 38, 8, 40, 10, 42, 12, 44, 14, 46, 16, 48, 18, 50, 20, 52, 22, 54, 24, 56, 26, 58, 28, 60, 30, 62); OverlayWords alpha = __builtin_shufflevector(a, a, 0, 0, 2, 2, 4, 4, 6, 6, 8, 8, 10, 10, 12, 12, 14, 14, 16, 16, 18, 18, 20, 20, 22, 22, 24, 24, 26, 26, 28, 28, 30, 30); OverlayWords base = overlay_load_vector(src + p->src_uv + (y / 2) * p->src_stride + x * bytes, wide, (p->width - x) * bytes); OverlayWords value = overlay_div255(base * (1020 - alpha) + 128 * scale * alpha + uv * scale + 510) >> 2; overlay_store_vector(dst + p->dst_uv + (y / 2) * p->dst_stride + x * bytes, value, wide); } static void overlay_rows(const OverlayParams *p, const uint8_t *src, const uint8_t *rgb, uint8_t *dst, int worker, int workers, int hvx) { int bytes = p->flags & OVERLAY_P010 ? 2 : 1; int left = overlay_clamp(p->x & ~1, 0, p->width), right = overlay_clamp((p->x + p->w + 1) & ~1, 0, p->width); // Interleave chroma rows so a subtitle near the bottom uses every worker. for (int y = worker * 2; y < p->height; y += workers * 2) { int active = p->alpha && y < p->y + p->h && y + 2 > p->y && right > left; int start = active ? left : p->width, stop = active ? right : p->width; for (int row = 0; row < 3; ++row) { const uint8_t *in = src + (row < 2 ? p->src_y + (y + row) * p->src_stride : p->src_uv + (y / 2) * p->src_stride); uint8_t *out = dst + (row < 2 ? p->dst_y + (y + row) * p->dst_stride : p->dst_uv + (y / 2) * p->dst_stride); if (in != out) { overlay_copy(out, in, start * bytes, hvx); overlay_copy(out + stop * bytes, in + stop * bytes, (p->width - stop) * bytes, hvx); } } for (int x = start; x < stop;) { if (hvx && p->w >= p->rgb_width && p->rgb_width >= 32 && x >= p->x && x + 32 <= p->x + p->w && x + 32 <= stop && y >= p->y && y + 2 <= p->y + p->h) { int first = overlay_clamp((x - p->x) * p->step_x + p->step_x / 2 - 32768, 0, (p->rgb_width - 1) << 16) >> 16; int last = overlay_clamp((x + 31 - p->x) * p->step_x + p->step_x / 2 - 32768, 0, (p->rgb_width - 1) << 16) >> 16; if ((p->w != p->rgb_width || p->h != p->rgb_height) && last - first >= 31) { overlay_block(p, src, rgb, dst, x, y); x += 2; continue; } overlay_vector(p, src, rgb, dst, x, y); x += 32; } else { overlay_block(p, src, rgb, dst, x, y); x += 2; } } } } #endif qcom-va-driver-1.1.0/src/cdsp/qurt_abi.h000066400000000000000000000103221526764642600200640ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #pragma once // Private unsigned CDSP ABI subset, checked against SDK 6.7 for // v68, v69, v73, v75, v79 and v81. // Firmware provides these functions; no QuRT implementation is linked here. #include #include #include typedef unsigned int qurt_thread_t; typedef struct { char name[16]; uint8_t tcb_partition, stid; uint16_t priority; uint8_t autostack : 1, group_id : 6, reserved : 1; uint8_t bus_priority; uint16_t timetest_id; uint32_t stack_size; void *stack_addr; uint16_t detach_state; } qurt_thread_attr_t; typedef union { uint64_t opaque[2]; } qurt_sem_t; #define QURT_THREAD_ATTR_PRIORITY_DEFAULT 254 #define QURT_EOK 0 #define QURT_HVX_MODE_128B 1 int qurt_thread_create(qurt_thread_t *, qurt_thread_attr_t *, void (*)(void *), void *); void qurt_thread_exit(int); int qurt_thread_join(qurt_thread_t, int *); void qurt_sem_init_val(qurt_sem_t *, unsigned short); void qurt_sem_destroy(qurt_sem_t *); int qurt_sem_down(qurt_sem_t *); int qurt_sem_add(qurt_sem_t *, unsigned int); int qurt_hvx_lock(int); int qurt_hvx_unlock(void); int qurt_hvx_get_units(void); static inline int qurt_sem_up(qurt_sem_t *s) { return qurt_sem_add(s, 1); } static inline void qurt_thread_attr_init(qurt_thread_attr_t *a) { memset(a, 0, sizeof(*a)); a->priority = QURT_THREAD_ATTR_PRIORITY_DEFAULT; a->bus_priority = 255; a->timetest_id = (uint16_t)-2; } static inline void qurt_thread_attr_set_stack_addr(qurt_thread_attr_t *a, void *s) { a->stack_addr = s; } static inline void qurt_thread_attr_set_stack_size(qurt_thread_attr_t *a, unsigned s) { a->stack_size = s; } static inline void qurt_thread_attr_set_priority(qurt_thread_attr_t *a, unsigned short p) { a->priority = p; } static inline void qurt_thread_attr_set_name(qurt_thread_attr_t *a, const char *s) { size_t n = strlen(s); if (n >= sizeof(a->name)) n = sizeof(a->name) - 1; memcpy(a->name, s, n); a->name[n] = 0; } static inline void qurt_mem_syncht(void) { __asm__ volatile("syncht" ::: "memory"); } static inline uint64_t HAP_perf_get_time_us(void) { uint64_t ticks; __asm__ volatile("%0 = c31:30" : "=r"(ticks)); return ticks * 10 / 192; // 19.2 MHz architectural timer. } #define HAP_power_set_HVX 2 #define HAP_power_set_apptype 3 #define HAP_power_set_DCVS_v3 12 #define HAP_power_get_clk_Freq 4 #define HAP_POWER_COMPUTE_CLIENT_CLASS 4 #define HAP_DCVS_V2_PERFORMANCE_MODE 0x10 #define HAP_DCVS_VCORNER_MAX 255 #define HAP_DCVS_LPM_LEVEL1 1 #ifndef TRUE #define TRUE 1 #endif typedef struct { uint8_t target_corner, min_corner, max_corner; uint32_t reserved[3]; } IrisPowerCorners; typedef struct { uint8_t set_dcvs_enable, dcvs_enable; uint8_t dcvs_option; uint8_t set_latency; uint32_t latency; uint8_t set_core_params; IrisPowerCorners core_params; uint8_t set_bus_params; IrisPowerCorners bus_params; uint8_t set_dcvs_v3_params; uint32_t reserved[6]; uint8_t set_sleep_disable, sleep_disable; } IrisPowerDcvs; typedef struct { uint8_t type; union { uint64_t reserved[14]; uint8_t apptype; struct { uint8_t power_up; } hvx; IrisPowerDcvs dcvs_v3; }; } HAP_power_request_t; typedef struct { uint8_t type; union { uint64_t reserved; uint32_t clkFreqHz; }; } HAP_power_response_t; int HAP_power_set(void *, HAP_power_request_t *); int HAP_power_get(void *, HAP_power_response_t *); int HAP_power_destroy_client(void *); // Optional compute-resource ABI behind the SDK's HAP_compute_res wrappers. // Weak imports let older firmware load the skeleton without VTCM support. typedef struct { unsigned long long attributes[20]; } compute_res_attr_t; int compute_resource_attr_init(compute_res_attr_t *) __attribute__((weak)); int compute_resource_attr_set_vtcm_param(compute_res_attr_t *, unsigned int, unsigned char) __attribute__((weak)); unsigned int compute_resource_acquire(compute_res_attr_t *, unsigned int) __attribute__((weak)); void *compute_resource_attr_get_vtcm_ptr(compute_res_attr_t *) __attribute__((weak)); int compute_resource_release(unsigned int) __attribute__((weak)); #include "qurt_abi_checks.h" qcom-va-driver-1.1.0/src/cdsp/qurt_abi_checks.h000066400000000000000000000063601526764642600214130ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Also compiled against the official SDK by tests/vpp/check-abi.py. #include #define ABI_SIZE(t, s, a) _Static_assert(sizeof(t) == (s) && _Alignof(t) == (a), #t " ABI") #define ABI_OFFSET(t, f, o) _Static_assert(offsetof(t, f) == (o), #t "." #f " ABI") ABI_SIZE(qurt_thread_attr_t, 36, 4); ABI_OFFSET(qurt_thread_attr_t, stid, 17); ABI_OFFSET(qurt_thread_attr_t, priority, 18); ABI_OFFSET(qurt_thread_attr_t, bus_priority, 21); ABI_OFFSET(qurt_thread_attr_t, timetest_id, 22); ABI_OFFSET(qurt_thread_attr_t, stack_size, 24); ABI_OFFSET(qurt_thread_attr_t, stack_addr, 28); ABI_OFFSET(qurt_thread_attr_t, detach_state, 32); ABI_SIZE(qurt_sem_t, 16, 8); ABI_SIZE(compute_res_attr_t, 160, 8); ABI_OFFSET(compute_res_attr_t, attributes, 0); #define ABI_FUNCTION(name, type) \ _Static_assert(__builtin_types_compatible_p(__typeof__(&name), type), #name " ABI") ABI_FUNCTION(compute_resource_attr_init, int (*)(compute_res_attr_t *)); ABI_FUNCTION(compute_resource_attr_set_vtcm_param, int (*)(compute_res_attr_t *, unsigned int, unsigned char)); ABI_FUNCTION(compute_resource_acquire, unsigned int (*)(compute_res_attr_t *, unsigned int)); ABI_FUNCTION(compute_resource_attr_get_vtcm_ptr, void *(*)(compute_res_attr_t *)); ABI_FUNCTION(compute_resource_release, int (*)(unsigned int)); #undef ABI_FUNCTION ABI_SIZE(HAP_power_request_t, 120, 8); ABI_SIZE(HAP_power_response_t, 16, 8); ABI_OFFSET(HAP_power_request_t, apptype, 8); ABI_OFFSET(HAP_power_request_t, hvx.power_up, 8); ABI_OFFSET(HAP_power_request_t, dcvs_v3, 8); ABI_OFFSET(HAP_power_request_t, dcvs_v3.set_dcvs_enable, 8); ABI_OFFSET(HAP_power_request_t, dcvs_v3.dcvs_enable, 9); ABI_OFFSET(HAP_power_request_t, dcvs_v3.dcvs_option, 10); ABI_OFFSET(HAP_power_request_t, dcvs_v3.set_latency, 11); ABI_OFFSET(HAP_power_request_t, dcvs_v3.latency, 12); ABI_OFFSET(HAP_power_request_t, dcvs_v3.set_core_params, 16); ABI_OFFSET(HAP_power_request_t, dcvs_v3.core_params, 20); ABI_OFFSET(HAP_power_request_t, dcvs_v3.core_params.target_corner, 20); ABI_OFFSET(HAP_power_request_t, dcvs_v3.core_params.min_corner, 21); ABI_OFFSET(HAP_power_request_t, dcvs_v3.core_params.max_corner, 22); ABI_OFFSET(HAP_power_request_t, dcvs_v3.set_bus_params, 36); ABI_OFFSET(HAP_power_request_t, dcvs_v3.bus_params, 40); ABI_OFFSET(HAP_power_request_t, dcvs_v3.bus_params.target_corner, 40); ABI_OFFSET(HAP_power_request_t, dcvs_v3.bus_params.min_corner, 41); ABI_OFFSET(HAP_power_request_t, dcvs_v3.bus_params.max_corner, 42); ABI_OFFSET(HAP_power_request_t, dcvs_v3.set_dcvs_v3_params, 56); ABI_OFFSET(HAP_power_request_t, dcvs_v3.set_sleep_disable, 84); ABI_OFFSET(HAP_power_request_t, dcvs_v3.sleep_disable, 85); ABI_OFFSET(HAP_power_response_t, clkFreqHz, 8); _Static_assert(HAP_power_set_HVX == 2 && HAP_power_set_apptype == 3 && HAP_power_set_DCVS_v3 == 12 && HAP_power_get_clk_Freq == 4 && HAP_POWER_COMPUTE_CLIENT_CLASS == 4 && HAP_DCVS_V2_PERFORMANCE_MODE == 16 && HAP_DCVS_VCORNER_MAX == 255 && HAP_DCVS_LPM_LEVEL1 == 1 && QURT_HVX_MODE_128B == 1 && QURT_THREAD_ATTR_PRIORITY_DEFAULT == 254, "firmware constants"); #undef ABI_SIZE #undef ABI_OFFSET qcom-va-driver-1.1.0/src/cdsp/skel.cpp000066400000000000000000000056331526764642600175600ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "skel.h" #include "iris_vpp.h" #include "vpp_limits.h" #include #include #include int cdsp_vpp_select_workers(int *workers) { if (!workers || *workers < 0 || *workers > CDSP_VPP_MAX_WORKERS) return -EINVAL; // Zero explicitly requests the scalar DSP diagnostic path. if (!*workers) return 0; fastrpc_capability request{CDSP_DOMAIN_ID, HVX_SUPPORT_128B, 0}; int e = remote_handle_control(DSPRPC_GET_DSP_INFO, &request, sizeof(request)); if (e) { fprintf(stderr, "CDSP VPP: HVX_SUPPORT_128B query failed: %d\n", e); return e; } if (!request.capability) { fprintf(stderr, "CDSP VPP: 128-byte HVX is unavailable\n"); return -ENOTSUP; } // The kernels support any count up to CDSP_VPP_MAX_WORKERS. The request is a ceiling; // never dispatch more workers than the reported number of 128-byte contexts. if ((uint32_t)*workers > request.capability) *workers = (int)request.capability; return 0; } int cdsp_vpp_open_skel(remote_handle64 *handle) { fastrpc_capability request{CDSP_DOMAIN_ID, ARCH_VER, 0}; int e = remote_handle_control(DSPRPC_GET_DSP_INFO, &request, sizeof(request)); if (e) { fprintf(stderr, "CDSP VPP: ARCH_VER query failed: %d\n", e); return e; } // Like cdsp_peak, accept decimal versions and packed hexadecimal versions // (including revision bits above the low byte). Reject malformed BCD digits. unsigned arch = request.capability; unsigned low = arch & 0xff; if (!(arch >= 60 && arch <= 99) && low >= 0x60 && low <= 0x99 && (low & 0xf) <= 9) arch = (low >> 4) * 10 + (low & 0xf); switch (arch) { case 68: case 69: case 73: case 75: case 79: case 81: break; default: fprintf(stderr, "CDSP VPP: unsupported ARCH_VER 0x%x; no matching skeleton\n", request.capability); return -ENOTSUP; } constexpr char suffix[] = "?iris_vpp_skel_handle_invoke&_modver=1.0&_dom=cdsp"; char uri[sizeof(IRIS_VPP_DSP_DIR) + sizeof(suffix) + 64]; // FastRPC's file:/// prefix is followed by the module path; an absolute // install path therefore starts with four slashes. Prefer it unless the // caller explicitly supplies a search path (e.g. for an uninstalled build). const char *search_path = getenv("ADSP_LIBRARY_PATH"); bool search_first = search_path && *search_path; auto open = [&](bool search) { snprintf(uri, sizeof(uri), search ? "file:///vpp_v%u_skel.so%s" : "file:///" IRIS_VPP_DSP_DIR "/vpp_v%u_skel.so%s", arch, suffix); return iris_vpp_open(uri, handle); }; e = open(search_first); if (e) e = open(!search_first); if (e) fprintf(stderr, "CDSP VPP: cannot load vpp_v%u_skel.so: %d\n", arch, e); return e; } qcom-va-driver-1.1.0/src/cdsp/skel.h000066400000000000000000000002361526764642600172170ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #pragma once #include int cdsp_vpp_open_skel(remote_handle64 *handle); int cdsp_vpp_select_workers(int *workers); qcom-va-driver-1.1.0/src/cdsp/vpp_limits.h000066400000000000000000000004631526764642600204510ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #pragma once #define CDSP_VPP_MAX_WORKERS 8 #define CDSP_VPP_PROFILE_COUNTERS (CDSP_VPP_MAX_WORKERS * 4) #define CDSP_VPP_NV12 0 #define CDSP_VPP_P010 1 #define CDSP_VPP_ROTATE_NONE 0 #define CDSP_VPP_ROTATE_90 1 #define CDSP_VPP_ROTATE_180 2 #define CDSP_VPP_ROTATE_270 3 qcom-va-driver-1.1.0/src/cpu-copy.cpp000066400000000000000000000016731526764642600174300ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "internal.hpp" namespace irisva { struct SurfaceCopier::Impl { Impl() { const char *mode = std::getenv("IRIS_VAAPI_COPY"); check(!mode || !strcmp(mode, "gpu") || !strcmp(mode, "cpu"), "IRIS_VAAPI_COPY must be gpu or cpu", VA_STATUS_ERROR_INVALID_PARAMETER); check(!mode || strcmp(mode, "gpu"), "GPU copy support was disabled at build time; use IRIS_VAAPI_COPY=cpu", VA_STATUS_ERROR_UNIMPLEMENTED); trace("surface copy selected mode=%s", "cpu"); } }; SurfaceCopier::SurfaceCopier(int render_fd) : render_fd_(render_fd) {} SurfaceCopier::~SurfaceCopier() = default; void SurfaceCopier::copy(const std::shared_ptr &dst, const std::shared_ptr &src, unsigned w, unsigned h) { if (!impl_) impl_ = std::make_unique(); copy_surface(*dst, *src, w, h); } } // namespace irisva qcom-va-driver-1.1.0/src/decoder.cpp000066400000000000000000000461611526764642600172770ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "internal.hpp" #include #include #include #include #include #include #include #include #ifndef V4L2_PIX_FMT_AV1 #define V4L2_PIX_FMT_AV1 v4l2_fourcc('A', 'V', '0', '1') #endif namespace irisva { namespace { constexpr unsigned OUTPUT = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; constexpr unsigned CAPTURE = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; int call(int fd, unsigned long command, void *p) { int r; do { r = ioctl(fd, command, p); } while (r < 0 && errno == EINTR); return r; } void checked(int fd, unsigned long command, void *p, const char *name) { if (call(fd, command, p) < 0) throw Error(VA_STATUS_ERROR_OPERATION_FAILED, std::string(name) + ": " + strerror(errno)); } struct QueueBuffer { v4l2_plane plane{}; v4l2_buffer buffer{}; explicit QueueBuffer(unsigned type, unsigned index = 0) { buffer.type = type; buffer.index = index; buffer.memory = V4L2_MEMORY_MMAP; buffer.length = 1; buffer.m.planes = &plane; } }; bool expired(std::chrono::steady_clock::time_point start) { return std::chrono::steady_clock::now() - start > std::chrono::seconds(5); } } // namespace Memory::~Memory() { // Release device imports while their backing storage is still alive. vpp_registration.reset(); vulkan_registration.reset(); if (mapping) munmap(mapping, size); if (fd >= 0) close(fd); } uint8_t *Memory::map() { if (!mapping) { void *p = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); check(p != MAP_FAILED, "mmap DMA-BUF failed"); mapping = p; } return static_cast(mapping); } std::string find_device(bool &av1) { std::vector paths; if (const char *p = std::getenv("IRIS_VAAPI_DEVICE")) paths.push_back(p); else { glob_t g{}; if (!glob("/dev/video*", 0, nullptr, &g)) for (size_t i = 0; i < g.gl_pathc; ++i) paths.emplace_back(g.gl_pathv[i]); globfree(&g); } for (const auto &path : paths) { int fd = open(path.c_str(), O_RDWR | O_NONBLOCK | O_CLOEXEC); if (fd < 0) continue; v4l2_capability cap{}; bool match = false, has_av1 = false; if (!call(fd, VIDIOC_QUERYCAP, &cap) && !strcmp(reinterpret_cast(cap.driver), "iris_driver")) { unsigned caps = cap.capabilities & V4L2_CAP_DEVICE_CAPS ? cap.device_caps : cap.capabilities; if (caps & V4L2_CAP_VIDEO_M2M_MPLANE) { v4l2_fmtdesc fmt{}; fmt.type = OUTPUT; while (!call(fd, VIDIOC_ENUM_FMT, &fmt)) { if (fmt.pixelformat == V4L2_PIX_FMT_H264) match = true; if (fmt.pixelformat == V4L2_PIX_FMT_AV1) has_av1 = true; ++fmt.index; } } } close(fd); if (match) { av1 = has_av1; return path; } } throw Error(VA_STATUS_ERROR_OPERATION_FAILED, "no Iris stateful H264 decoder found"); } Decoder::Decoder(const std::string &device, unsigned width, unsigned height, unsigned surfaces, VAProfile profile, unsigned rt_format, int render_fd) : render_fd_(render_fd), width_(width), height_(height), preferred_pool_size_(std::min(64u, std::max(32u, surfaces + 4))), fourcc_(rt_format == VA_RT_FORMAT_YUV420_10 ? VA_FOURCC_P010 : VA_FOURCC_NV12), copier_(render_fd) { fd_ = open(device.c_str(), O_RDWR | O_NONBLOCK | O_CLOEXEC); check(fd_ >= 0, "open Iris decoder"); try { v4l2_event_subscription sub{}; sub.type = V4L2_EVENT_SOURCE_CHANGE; checked(fd_, VIDIOC_SUBSCRIBE_EVENT, &sub, "SUBSCRIBE SOURCE_CHANGE"); v4l2_format fmt{}; fmt.type = OUTPUT; fmt.fmt.pix_mp.width = width; fmt.fmt.pix_mp.height = height; fmt.fmt.pix_mp.pixelformat = is_av1(profile) ? V4L2_PIX_FMT_AV1 : is_vp9(profile) ? V4L2_PIX_FMT_VP9 : is_hevc(profile) ? V4L2_PIX_FMT_HEVC : V4L2_PIX_FMT_H264; unsigned codec = fmt.fmt.pix_mp.pixelformat; fmt.fmt.pix_mp.num_planes = 1; checked(fd_, VIDIOC_S_FMT, &fmt, "S_FMT OUTPUT"); check(fmt.fmt.pix_mp.pixelformat == codec, "Iris does not support the requested codec"); v4l2_control delay{}; delay.id = V4L2_CID_MPEG_VIDEO_DEC_DISPLAY_DELAY; delay.value = 0; checked(fd_, VIDIOC_S_CTRL, &delay, "S_CTRL DISPLAY_DELAY (Iris decode-order patch required)"); delay.id = V4L2_CID_MPEG_VIDEO_DEC_DISPLAY_DELAY_ENABLE; delay.value = 1; checked(fd_, VIDIOC_S_CTRL, &delay, "S_CTRL DISPLAY_DELAY_ENABLE"); allocate(OUTPUT, 4, output_); unsigned type = OUTPUT; checked(fd_, VIDIOC_STREAMON, &type, "STREAMON OUTPUT"); output_on_ = true; if (const char *path = std::getenv("IRIS_VAAPI_DUMP")) dump_ = fopen(path, "wb"); trace("Iris session %s %ux%u output-buffers=%zu", device.c_str(), width, height, output_.size()); } catch (...) { close(fd_); fd_ = -1; throw; } } Decoder::~Decoder() { if (dump_) fclose(dump_); fail(VA_STATUS_ERROR_DECODING_ERROR); } void Decoder::fail(VAStatus status) { failure_ = status; if (fd_ >= 0) { unsigned type = OUTPUT; if (output_on_) call(fd_, VIDIOC_STREAMOFF, &type); type = CAPTURE; if (capture_on_) call(fd_, VIDIOC_STREAMOFF, &type); close(fd_); fd_ = -1; } output_on_ = capture_on_ = source_change_ = false; for (auto &[token, surface] : pending_) { surface->status = status; surface->pending = false; } pending_.clear(); capture_.clear(); output_.clear(); } void Decoder::allocate(unsigned type, unsigned count, std::vector> &pool) { v4l2_requestbuffers req{}; req.type = type; req.memory = V4L2_MEMORY_MMAP; req.count = count; checked(fd_, VIDIOC_REQBUFS, &req, "REQBUFS"); check(req.count > 0 && req.count <= 64, "invalid allocated buffer count"); for (unsigned i = 0; i < req.count; ++i) { QueueBuffer q(type, i); checked(fd_, VIDIOC_QUERYBUF, &q.buffer, "QUERYBUF"); auto m = std::make_shared(); m->origin = MemoryOrigin::Iris; m->index = i; m->size = q.plane.length; if (type == OUTPUT) { void *p = mmap(nullptr, m->size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, q.plane.m.mem_offset); check(p != MAP_FAILED, "mmap OUTPUT"); m->mapping = p; } else { v4l2_exportbuffer exp{}; exp.type = type; exp.index = i; exp.flags = O_CLOEXEC | O_RDWR; checked(fd_, VIDIOC_EXPBUF, &exp, "EXPBUF"); m->fd = exp.fd; } pool.push_back(m); } } void Decoder::configure_capture() { check(!capture_on_, "in-context dynamic resolution change not yet implemented", VA_STATUS_ERROR_UNIMPLEMENTED); v4l2_format fmt{}; fmt.type = CAPTURE; checked(fd_, VIDIOC_G_FMT, &fmt, "G_FMT CAPTURE"); unsigned pixel_format = fourcc_ == VA_FOURCC_P010 ? V4L2_PIX_FMT_P010 : V4L2_PIX_FMT_NV12; fmt.fmt.pix_mp.pixelformat = pixel_format; checked(fd_, VIDIOC_S_FMT, &fmt, "S_FMT CAPTURE"); check(fmt.fmt.pix_mp.pixelformat == pixel_format && fmt.fmt.pix_mp.num_planes == 1, "unsupported CAPTURE layout"); v4l2_control minimum{}; minimum.id = V4L2_CID_MIN_BUFFERS_FOR_CAPTURE; checked(fd_, VIDIOC_G_CTRL, &minimum, "G_CTRL minimum capture buffers"); // VA clients can retain output in reference/reorder/renderer queues. Treat // their pool sizes as hints: at 8K, blindly reserving 32+ buffers can exhaust // Iris's IOVA space before STREAMON allocates the internal buffers. unsigned count = capture_pool_size(preferred_pool_size_, minimum.value, fmt.fmt.pix_mp.plane_fmt[0].sizeimage); trace("CAPTURE pool preferred=%u target=%u firmware-min=%d bytes-per-buffer=%u", preferred_pool_size_, count, minimum.value, fmt.fmt.pix_mp.plane_fmt[0].sizeimage); allocate(CAPTURE, count, capture_); check(capture_.size() >= count, "CAPTURE pool too small; apply the Iris gen2 capture-pool kernel patch", VA_STATUS_ERROR_ALLOCATION_FAILED); for (auto &m : capture_) { m->stride = fmt.fmt.pix_mp.plane_fmt[0].bytesperline; m->storage_height = fmt.fmt.pix_mp.height; m->width = fmt.fmt.pix_mp.width; m->height = fmt.fmt.pix_mp.height; m->fourcc = fourcc_; } requeue(); unsigned type = CAPTURE; checked(fd_, VIDIOC_STREAMON, &type, "STREAMON CAPTURE"); capture_on_ = true; source_change_ = false; trace("CAPTURE %ux%u pitch=%u count=%zu", fmt.fmt.pix_mp.width, fmt.fmt.pix_mp.height, fmt.fmt.pix_mp.plane_fmt[0].bytesperline, capture_.size()); } void Decoder::requeue() { if (last_) return; for (auto &m : capture_) { if (!m->queued && m.use_count() == 1) { QueueBuffer q(CAPTURE, m->index); q.plane.length = m->size; checked(fd_, VIDIOC_QBUF, &q.buffer, "QBUF CAPTURE"); m->queued = true; } } } void Decoder::recover_picture(uint64_t token) { auto it = pending_.find(token); if (it == pending_.end()) return; // The other queue may already have completed this picture. auto surface = it->second; surface->status = VA_STATUS_ERROR_DECODING_ERROR; try { // CAPTURE storage can still be a firmware reference. Replace only the // client-visible image, keeping previously exported/imported storage. auto memory = surface->persistent_export ? surface->memory : allocate_surface(render_fd_, surface->width, surface->height, fourcc_); check(bool(memory), "persistent surface lost its storage"); fill_neutral_surface(*memory, surface->width, surface->height); surface->memory = memory; surface->status = VA_STATUS_SUCCESS; std::fprintf(stderr, "iris-vaapi: replaced damaged picture token=%llu with neutral gray\n", (unsigned long long)token); } catch (const Error &e) { surface->status = e.status; } catch (const std::bad_alloc &) { surface->status = VA_STATUS_ERROR_ALLOCATION_FAILED; } surface->pending = false; pending_.erase(it); trace("completed damaged token=%llu status=%#x", (unsigned long long)token, unsigned(surface->status)); } void Decoder::pump(int timeout_ms) { check(failure_ == VA_STATUS_SUCCESS, "decoder session has failed", failure_); requeue(); if (timeout_ms) { pollfd p{fd_, POLLIN | POLLPRI | POLLOUT, 0}; int ret; do { ret = poll(&p, 1, timeout_ms); } while (ret < 0 && errno == EINTR); check(ret >= 0 && !(p.revents & POLLNVAL), "poll Iris failed"); } for (;;) { v4l2_event event{}; if (call(fd_, VIDIOC_DQEVENT, &event) < 0) { check(errno == EAGAIN || errno == ENOENT, "DQEVENT failed"); break; } if (event.type == V4L2_EVENT_SOURCE_CHANGE) source_change_ = true; } if (source_change_ && !capture_on_) { try { configure_capture(); } catch (const Error &e) { // STREAMON failure can leave the kernel session unusable. Close it // and complete pending surfaces instead of retrying S_FMT forever. fail(e.status); throw; } catch (...) { fail(VA_STATUS_ERROR_ALLOCATION_FAILED); throw; } } for (;;) { QueueBuffer q(OUTPUT); if (call(fd_, VIDIOC_DQBUF, &q.buffer) < 0) { check(errno == EAGAIN, "DQBUF OUTPUT failed"); break; } check(q.buffer.index < output_.size(), "invalid OUTPUT index"); output_[q.buffer.index]->queued = false; if (q.buffer.flags & V4L2_BUF_FLAG_ERROR) { uint64_t token = uint64_t(q.buffer.timestamp.tv_sec) * 1000000 + q.buffer.timestamp.tv_usec; recover_picture(token); } } if (!capture_on_ || last_) return; for (;;) { QueueBuffer q(CAPTURE); if (call(fd_, VIDIOC_DQBUF, &q.buffer) < 0) { check(errno == EAGAIN || errno == EPIPE, "DQBUF CAPTURE failed"); break; } check(q.buffer.index < capture_.size(), "invalid CAPTURE index"); auto &m = capture_[q.buffer.index]; m->queued = false; trace("CAPTURE index=%u used=%u offset=%u ts=%lld:%lld flags=%x", q.buffer.index, q.plane.bytesused, q.plane.data_offset, (long long)q.buffer.timestamp.tv_sec, (long long)q.buffer.timestamp.tv_usec, q.buffer.flags); uint64_t token = uint64_t(q.buffer.timestamp.tv_sec) * 1000000 + q.buffer.timestamp.tv_usec; if (q.buffer.flags & V4L2_BUF_FLAG_ERROR) { // An empty error buffer still completes the picture identified by // its timestamp. Older kernels clear it; do not guess that frame. recover_picture(token); } else if (q.plane.bytesused > q.plane.data_offset) { auto it = pending_.find(token); check(token && token < next_token_, "unmatched CAPTURE timestamp", VA_STATUS_ERROR_DECODING_ERROR); if (it == pending_.end()) { // OUTPUT may have reported an error before CAPTURE returned. trace("discarding completed token=%llu", (unsigned long long)token); } else { auto surface = it->second; m->data_offset = q.plane.data_offset; surface->status = VA_STATUS_SUCCESS; if (surface->persistent_export) { // Keep the fd/offset/pitch which the client already imported. // CAPTURE selection is owned by the stateful decoder, not VA. try { check(bool(surface->memory), "persistent export lost its storage"); copier_.copy(surface->memory, m, surface->width, surface->height); } catch (const Error &e) { surface->status = e.status; surface->pending = false; pending_.erase(it); throw; } } else { surface->memory = m; } surface->pending = false; trace("decoded token=%llu capture=%u flags=%x", (unsigned long long)token, q.buffer.index, q.buffer.flags); pending_.erase(it); } } if (q.buffer.flags & V4L2_BUF_FLAG_LAST) { last_ = true; break; } } check(!source_change_, "in-context dynamic resolution change not yet implemented", VA_STATUS_ERROR_UNIMPLEMENTED); } void Decoder::submit(const std::vector &bytes, const std::shared_ptr &surface) { std::lock_guard lock(mutex_); // VLC allocates its renderer pool before creating the FFmpeg context, // which can pass zero render targets to VA. Preserve the allocation hint // on each surface as a preference, subject to the CAPTURE memory budget. if (!capture_on_) preferred_pool_size_ = std::min(64u, std::max(preferred_pool_size_, surface->allocation_count + 4)); pump(0); auto start = std::chrono::steady_clock::now(); std::shared_ptr slot; while (!slot) { for (auto &m : output_) if (!m->queued) { slot = m; break; } if (slot) break; check(!expired(start), "OUTPUT queue timeout", VA_STATUS_ERROR_TIMEDOUT); pump(20); } check(!bytes.empty() && bytes.size() <= slot->size, "compressed picture too large", VA_STATUS_ERROR_INVALID_BUFFER); std::memcpy(slot->mapping, bytes.data(), bytes.size()); if (dump_) { fwrite(bytes.data(), 1, bytes.size(), dump_); fflush(dump_); } uint64_t token = next_token_; QueueBuffer q(OUTPUT, slot->index); q.plane.bytesused = bytes.size(); q.plane.length = slot->size; q.buffer.timestamp.tv_sec = token / 1000000; q.buffer.timestamp.tv_usec = token % 1000000; checked(fd_, VIDIOC_QBUF, &q.buffer, "QBUF OUTPUT"); ++next_token_; slot->queued = true; surface->pending = true; surface->token = token; surface->status = VA_STATUS_SUCCESS; pending_[token] = surface; trace("submit token=%llu bytes=%zu", (unsigned long long)token, bytes.size()); if (!capture_on_) { try { while (!capture_on_) { check(!expired(start), "initial SOURCE_CHANGE timeout", VA_STATUS_ERROR_TIMEDOUT); pump(20); } } catch (const Error &e) { fail(e.status); throw; } } } void Decoder::refresh() { std::unique_lock lock(mutex_, std::try_to_lock); if (lock.owns_lock() && failure_ == VA_STATUS_SUCCESS) pump(0); } void Decoder::finish() { std::lock_guard lock(mutex_); if (failure_ != VA_STATUS_SUCCESS) return; auto start = std::chrono::steady_clock::now(); while (!pending_.empty()) { check(!expired(start), "pending pictures at context destruction", VA_STATUS_ERROR_TIMEDOUT); pump(20); } } void Decoder::sync(const std::shared_ptr &surface) { std::unique_lock lock(mutex_); pump(0); auto start = std::chrono::steady_clock::now(); while (surface->pending) { if (expired(start)) { unsigned queued = 0, retained = 0; for (auto &m : capture_) { queued += m->queued; retained += m.use_count() > 1; } trace("sync timeout token=%llu pending=%zu capture=%zu queued=%u retained=%u", (unsigned long long)surface->token, pending_.size(), capture_.size(), queued, retained); throw Error(VA_STATUS_ERROR_TIMEDOUT, "surface sync timeout"); } // Decode-order output makes each submitted picture independently // synchronizable. Never issue STOP based on an idle-time heuristic: // vaSyncSurface is not an end-of-stream notification. lock.unlock(); pollfd p{fd_, POLLIN | POLLPRI, 0}; poll(&p, 1, 10); lock.lock(); pump(0); check(!last_ || !surface->pending, "surface missing at LAST", VA_STATUS_ERROR_DECODING_ERROR); } if (surface->status != VA_STATUS_SUCCESS) throw Error(surface->status, "surface decode failed"); } } // namespace irisva qcom-va-driver-1.1.0/src/encoder.cpp000066400000000000000000001300111526764642600172750ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "internal.hpp" #include #include #include #include #include #include #include #include #include #include namespace irisva { namespace { constexpr unsigned OUTPUT = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; constexpr unsigned CAPTURE = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; int call(int fd, unsigned long command, void *arg) { int ret; do { ret = ioctl(fd, command, arg); } while (ret < 0 && errno == EINTR); return ret; } void checked(int fd, unsigned long command, void *arg, const char *name) { if (call(fd, command, arg) < 0) throw Error(VA_STATUS_ERROR_ENCODING_ERROR, std::string(name) + ": " + strerror(errno)); } struct QueueBuffer { v4l2_plane plane{}; v4l2_buffer buffer{}; explicit QueueBuffer(unsigned type, unsigned index = 0, bool import = false) { buffer.type = type; buffer.index = index; buffer.memory = type == OUTPUT && import ? V4L2_MEMORY_DMABUF : V4L2_MEMORY_MMAP; buffer.length = 1; buffer.m.planes = &plane; } }; template T parameter(const Buffer &b, size_t offset = 0) { check(b.elements == 1 && b.data.size() >= offset + sizeof(T), "short encode parameter buffer", VA_STATUS_ERROR_INVALID_BUFFER); T value; std::memcpy(&value, b.data.data() + offset, sizeof(value)); return value; } int level_control(unsigned idc) { const unsigned levels[] = {10, 9, 11, 12, 13, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50, 51, 52, 60}; for (unsigned i = 0; i < std::size(levels); ++i) if (levels[i] == idc) return i; throw Error(VA_STATUS_ERROR_INVALID_PARAMETER, "unsupported H264 encode level"); } int hevc_level_control(unsigned idc) { const unsigned levels[] = {30, 60, 63, 90, 93, 120, 123, 150, 153, 156, 180, 183, 186}; for (unsigned i = 0; i < std::size(levels); ++i) if (levels[i] == idc) return i; throw Error(VA_STATUS_ERROR_INVALID_PARAMETER, "unsupported HEVC encode level"); } std::array deblocking(const EncodePicture &p, bool hevc) { if (hevc) { const auto &slice = p.hevc_slices.front(); bool disabled = slice.slice_fields.bits.slice_deblocking_filter_disabled_flag; int mode = slice.slice_fields.bits.slice_loop_filter_across_slices_enabled_flag ? 0 : 2; return {disabled ? 1 : mode, disabled ? 0 : slice.slice_beta_offset_div2, disabled ? 0 : slice.slice_tc_offset_div2}; } const auto &slice = p.slices.front(); // Offsets are absent from the slice header when filtering is disabled. return {slice.disable_deblocking_filter_idc, slice.disable_deblocking_filter_idc == 1 ? 0 : slice.slice_alpha_c0_offset_div2, slice.disable_deblocking_filter_idc == 1 ? 0 : slice.slice_beta_offset_div2}; } void validate_h264(const EncodeSettings &s, const EncodePicture &p, unsigned width, unsigned height) { check(s.has_sequence && p.has_params && p.slices.size() == 1, "encoding requires a sequence, picture and one slice", VA_STATUS_ERROR_INVALID_PARAMETER); const auto &seq = s.sequence; const auto &pic = p.params; const auto &slice = p.slices.front(); check(!pic.pic_fields.bits.idr_pic_flag || slice.slice_type % 5 == 2, "IDR requires an I slice", VA_STATUS_ERROR_UNIMPLEMENTED); check(seq.picture_width_in_mbs == (width + 15) / 16 && seq.picture_height_in_mbs == (height + 15) / 16, "encode sequence dimensions do not match context", VA_STATUS_ERROR_INVALID_PARAMETER); check(seq.seq_fields.bits.frame_mbs_only_flag && seq.seq_fields.bits.chroma_format_idc == 1 && !seq.bit_depth_luma_minus8 && !seq.bit_depth_chroma_minus8 && !seq.seq_fields.bits.seq_scaling_matrix_present_flag && seq.ip_period <= 1 && seq.max_num_ref_frames <= 1, "only progressive 8-bit H264 I/P encoding is supported", VA_STATUS_ERROR_UNIMPLEMENTED); check(slice.macroblock_address == 0 && slice.num_macroblocks == unsigned(seq.picture_width_in_mbs) * seq.picture_height_in_mbs && (slice.slice_type % 5 == 0 || slice.slice_type % 5 == 2), "only one full-picture I or P slice is supported", VA_STATUS_ERROR_UNIMPLEMENTED); check(!pic.pic_fields.bits.weighted_pred_flag && !pic.pic_fields.bits.weighted_bipred_idc && !pic.pic_fields.bits.pic_scaling_matrix_present_flag && !pic.chroma_qp_index_offset && !pic.second_chroma_qp_index_offset && !pic.num_ref_idx_l0_active_minus1 && !slice.num_ref_idx_l0_active_minus1 && !slice.cabac_init_idc, "unsupported H264 encode picture features", VA_STATUS_ERROR_UNIMPLEMENTED); const auto filter = deblocking(p, false); check(filter[0] <= 2 && filter[1] >= -6 && filter[1] <= 6 && filter[2] >= -6 && filter[2] <= 6, "invalid H264 encode deblocking request", VA_STATUS_ERROR_INVALID_PARAMETER); check(!seq.frame_crop_left_offset && !seq.frame_crop_top_offset && (!seq.frame_cropping_flag || (uint64_t(width) + uint64_t(seq.frame_crop_right_offset) * 2 == unsigned(seq.picture_width_in_mbs) * 16 && uint64_t(height) + uint64_t(seq.frame_crop_bottom_offset) * 2 == unsigned(seq.picture_height_in_mbs) * 16)), "unsupported encode cropping", VA_STATUS_ERROR_INVALID_PARAMETER); } void validate_hevc(const EncodeSettings &s, const EncodePicture &p, unsigned width, unsigned height) { check(s.has_sequence && p.has_params && p.hevc_slices.size() == 1, "encoding requires a sequence, picture and one slice", VA_STATUS_ERROR_INVALID_PARAMETER); const auto &seq = s.hevc_sequence; const auto &pic = p.hevc_params; const auto &slice = p.hevc_slices.front(); check(seq.general_profile_idc == 1 && seq.seq_fields.bits.chroma_format_idc == 1 && !seq.seq_fields.bits.bit_depth_luma_minus8 && !seq.seq_fields.bits.bit_depth_chroma_minus8 && !seq.seq_fields.bits.separate_colour_plane_flag && !seq.seq_fields.bits.pcm_enabled_flag && !seq.seq_fields.bits.hierachical_flag && !seq.vui_fields.bits.field_seq_flag && seq.ip_period <= 1, "only progressive 8-bit HEVC Main I/P encoding is supported", VA_STATUS_ERROR_UNIMPLEMENTED); check(seq.log2_min_luma_coding_block_size_minus3 <= 3 && seq.log2_diff_max_min_luma_coding_block_size <= 3 && seq.log2_min_luma_coding_block_size_minus3 + seq.log2_diff_max_min_luma_coding_block_size <= 3, "invalid HEVC coding block sizes", VA_STATUS_ERROR_INVALID_PARAMETER); trace("HEVC sequence=%ux%u visible=%ux%u min-cb-log2=%u ctu-diff=%u", seq.pic_width_in_luma_samples, seq.pic_height_in_luma_samples, width, height, seq.log2_min_luma_coding_block_size_minus3 + 3, seq.log2_diff_max_min_luma_coding_block_size); unsigned min_cb = 1u << (seq.log2_min_luma_coding_block_size_minus3 + 3); unsigned ctu = min_cb << seq.log2_diff_max_min_luma_coding_block_size; // HEVC VA sequence parameters lack a conformance window. The input surface // and context supply visible dimensions; the sequence may include CU padding. // Older FFmpeg clients align to 16 even when their SPS declares 8-pixel CUs. check(seq.pic_width_in_luma_samples >= width && seq.pic_width_in_luma_samples - width < std::max(16u, min_cb) && seq.pic_height_in_luma_samples >= height && seq.pic_height_in_luma_samples - height < std::max(16u, min_cb), "HEVC sequence dimensions do not match input", VA_STATUS_ERROR_INVALID_PARAMETER); check(slice.slice_segment_address == 0 && slice.num_ctu_in_slice == ((seq.pic_width_in_luma_samples + ctu - 1) / ctu) * ((seq.pic_height_in_luma_samples + ctu - 1) / ctu) && (slice.slice_type == 1 || slice.slice_type == 2) && (!pic.pic_fields.bits.idr_pic_flag || slice.slice_type == 2) && pic.pic_fields.bits.coding_type == (slice.slice_type == 2 ? 1u : 2u), "only one full-picture HEVC I or P slice is supported", VA_STATUS_ERROR_UNIMPLEMENTED); check(!pic.pic_fields.bits.tiles_enabled_flag && !pic.num_tile_columns_minus1 && !pic.num_tile_rows_minus1 && !pic.pic_fields.bits.entropy_coding_sync_enabled_flag && !pic.pic_fields.bits.dependent_slice_segments_enabled_flag && !pic.pic_fields.bits.weighted_pred_flag && !pic.pic_fields.bits.weighted_bipred_flag && !pic.pic_fields.bits.transquant_bypass_enabled_flag && !pic.pic_fields.bits.scaling_list_data_present_flag && !pic.num_ref_idx_l0_default_active_minus1 && !slice.num_ref_idx_l0_active_minus1 && !pic.pps_cb_qp_offset && !pic.pps_cr_qp_offset && !slice.slice_cb_qp_offset && !slice.slice_cr_qp_offset && !slice.slice_fields.bits.dependent_slice_segment_flag && !slice.slice_fields.bits.colour_plane_id && !pic.ctu_max_bitsize_allowed, "unsupported HEVC encode picture features", VA_STATUS_ERROR_UNIMPLEMENTED); check(slice.slice_fields.bits.slice_deblocking_filter_disabled_flag <= 1 && slice.slice_beta_offset_div2 >= -6 && slice.slice_beta_offset_div2 <= 6 && slice.slice_tc_offset_div2 >= -6 && slice.slice_tc_offset_div2 <= 6 && seq.general_tier_flag <= 1 && (!seq.general_tier_flag || seq.general_level_idc >= 120), "invalid HEVC filter or tier", VA_STATUS_ERROR_INVALID_PARAMETER); } void validate(const EncodeSettings &s, const EncodePicture &p, unsigned width, unsigned height) { if (s.hevc) validate_hevc(s, p, width, height); else validate_h264(s, p, width, height); check(s.fps_num && s.fps_den && s.fps_num >= s.fps_den && uint64_t(s.fps_num) <= uint64_t(s.fps_den) * 240, "unsupported encode frame rate", VA_STATUS_ERROR_INVALID_PARAMETER); check(s.min_qp >= 1 && s.max_qp <= 51 && s.min_qp <= s.max_qp, "invalid encode QP limits", VA_STATUS_ERROR_INVALID_PARAMETER); if (s.rate_control != VA_RC_CQP) check(s.bitrate && s.bitrate <= s.peak_bitrate && s.peak_bitrate <= 245000000, "invalid encode bitrate", VA_STATUS_ERROR_INVALID_PARAMETER); } } // namespace std::string find_encoder_device() { std::vector paths; if (const char *p = std::getenv("IRIS_VAAPI_ENCODER_DEVICE")) paths.emplace_back(p); else { glob_t g{}; if (!glob("/dev/video*", 0, nullptr, &g)) for (size_t i = 0; i < g.gl_pathc; ++i) paths.emplace_back(g.gl_pathv[i]); globfree(&g); } for (const auto &path : paths) { int fd = open(path.c_str(), O_RDWR | O_NONBLOCK | O_CLOEXEC); if (fd < 0) continue; v4l2_capability cap{}; bool match = false; if (!call(fd, VIDIOC_QUERYCAP, &cap) && !strcmp(reinterpret_cast(cap.driver), "iris_driver") && (cap.device_caps & V4L2_CAP_VIDEO_M2M_MPLANE)) { v4l2_fmtdesc fmt{}; fmt.type = CAPTURE; while (!call(fd, VIDIOC_ENUM_FMT, &fmt)) { if (fmt.pixelformat == V4L2_PIX_FMT_H264) match = true; ++fmt.index; } } close(fd); if (match) return path; } return {}; } void render_encode_buffer(EncodeSettings &s, EncodePicture &p, const Buffer &b) { switch (b.type) { case VAEncSequenceParameterBufferType: if (s.hevc) { s.hevc_sequence = parameter(b); s.has_sequence = true; const auto &seq = s.hevc_sequence; if (seq.vui_parameters_present_flag && seq.vui_fields.bits.vui_timing_info_present_flag) { s.fps_num = seq.vui_time_scale; s.fps_den = seq.vui_num_units_in_tick; } if (!s.bitrate) s.bitrate = s.peak_bitrate = seq.bits_per_second; break; } s.sequence = parameter(b); s.has_sequence = true; if (s.sequence.vui_parameters_present_flag && s.sequence.vui_fields.bits.timing_info_present_flag) { check(s.sequence.num_units_in_tick <= std::numeric_limits::max() / 2, "invalid encoder timing", VA_STATUS_ERROR_INVALID_PARAMETER); s.fps_num = s.sequence.time_scale; s.fps_den = s.sequence.num_units_in_tick * 2; } if (!s.bitrate) s.bitrate = s.peak_bitrate = s.sequence.bits_per_second; break; case VAEncPictureParameterBufferType: if (s.hevc) p.hevc_params = parameter(b); else p.params = parameter(b); p.has_params = true; break; case VAEncSliceParameterBufferType: if (s.hevc) { check(b.element_size >= sizeof(VAEncSliceParameterBufferHEVC), "short HEVC encode slice", VA_STATUS_ERROR_INVALID_BUFFER); for (unsigned i = 0; i < b.elements; ++i) { VAEncSliceParameterBufferHEVC slice; std::memcpy(&slice, b.data.data() + size_t(i) * b.element_size, sizeof(slice)); p.hevc_slices.push_back(slice); } break; } check(b.element_size >= sizeof(VAEncSliceParameterBufferH264), "short encode slice", VA_STATUS_ERROR_INVALID_BUFFER); for (unsigned i = 0; i < b.elements; ++i) { VAEncSliceParameterBufferH264 slice; std::memcpy(&slice, b.data.data() + size_t(i) * b.element_size, sizeof(slice)); p.slices.push_back(slice); } break; case VAEncMiscParameterBufferType: { auto misc = parameter(b); const size_t off = offsetof(VAEncMiscParameterBuffer, data); switch (misc.type) { case VAEncMiscParameterTypeRateControl: { auto rc = parameter(b, off); check(!rc.rc_flags.bits.temporal_id && !rc.rc_flags.bits.reset && !rc.rc_flags.bits.enable_parallel_brc && !rc.rc_flags.bits.enable_dynamic_scaling && !rc.target_frame_size, "unsupported encode rate control feature", VA_STATUS_ERROR_UNIMPLEMENTED); check(rc.target_percentage <= 100, "invalid target bitrate percentage", VA_STATUS_ERROR_INVALID_PARAMETER); s.peak_bitrate = rc.bits_per_second; s.bitrate = s.rate_control == VA_RC_VBR && rc.target_percentage ? uint64_t(rc.bits_per_second) * rc.target_percentage / 100 : rc.bits_per_second; s.min_qp = rc.min_qp ? rc.min_qp : 1; s.max_qp = rc.max_qp ? rc.max_qp : 51; break; } case VAEncMiscParameterTypeFrameRate: { auto fr = parameter(b, off); check(!fr.framerate_flags.bits.temporal_id, "temporal layers unsupported", VA_STATUS_ERROR_UNIMPLEMENTED); s.fps_num = fr.framerate & 0xffff; s.fps_den = std::max(1u, fr.framerate >> 16); break; } case VAEncMiscParameterTypeQualityLevel: { auto quality = parameter(b, off); check(quality.quality_level <= 1, "unsupported encoder quality level", VA_STATUS_ERROR_INVALID_PARAMETER); break; } case VAEncMiscParameterTypeHRD: // Iris has no CPB size/fullness control. The firmware owns the // rate-control buffer; HRD values are advisory for this backend. (void)parameter(b, off); break; default: throw Error(VA_STATUS_ERROR_UNIMPLEMENTED, "unsupported encode misc parameter"); } break; } default: throw Error(VA_STATUS_ERROR_UNSUPPORTED_BUFFERTYPE, "unsupported encode buffer"); } } struct Encoder::Impl { int fd = -1, render_fd = -1; bool import = false; struct Job { std::shared_ptr task; std::shared_ptr output; std::shared_ptr memory; bool input_done = false, frame_done = false; }; std::vector> staging; std::vector> slots; std::map> jobs; int current_qp = -1; std::unique_ptr copier; unsigned width, height, stride = 0, storage_height = 0, input_size = 0, input_count = 0; bool output_on = false, capture_on = false, failed = false; uint64_t token = 0; VASurfaceID last_reference = VA_INVALID_SURFACE; EncodeSettings settings; unsigned entropy; std::array filter{}; std::vector> capture, raw; Impl(unsigned w, unsigned h, const EncodeSettings &s, unsigned cabac) : width(w), height(h), settings(s), entropy(cabac) {} ~Impl() { stop(VA_STATUS_ERROR_INVALID_CONTEXT); if (fd >= 0) close(fd); copier.reset(); if (render_fd >= 0) close(render_fd); } void control(unsigned id, int value) { v4l2_control ctrl{}; ctrl.id = id; ctrl.value = value; trace("encoder control id=%#x value=%d", id, value); checked(fd, VIDIOC_S_CTRL, &ctrl, "encoder S_CTRL"); } void configure_deblocking() { const int ids[] = {settings.hevc ? V4L2_CID_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE : V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_MODE, settings.hevc ? V4L2_CID_MPEG_VIDEO_HEVC_LF_BETA_OFFSET_DIV2 : V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_ALPHA, settings.hevc ? V4L2_CID_MPEG_VIDEO_HEVC_LF_TC_OFFSET_DIV2 : V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_BETA}; bool supported = true; for (auto id : ids) { v4l2_queryctrl query{}; query.id = id; if (call(fd, VIDIOC_QUERYCTRL, &query) < 0) { check(errno == EINVAL, "query encoder deblocking controls", VA_STATUS_ERROR_ENCODING_ERROR); supported = false; } else if (query.flags & V4L2_CTRL_FLAG_DISABLED) { supported = false; } } if (!supported) { // Older Iris kernels only expose firmware's enabled, 0/0 default. // Never silently replace a non-default request with that default. check((filter[0] == 0 || (settings.hevc && filter[0] == 2)) && !filter[1] && !filter[2], "deblocking request requires Iris loop-filter controls", VA_STATUS_ERROR_UNIMPLEMENTED); return; } // Firmware can override slice offsets: on the tested SC8280XP, CQP // retains 0/0 and CBR/VBR can adjust them per frame. Forward the request // without rewriting headers that describe the firmware's actual filtering. const int modes[] = {V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_ENABLED, V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_DISABLED, V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_DISABLED_AT_SLICE_BOUNDARY}; const int hevc_modes[] = {V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_ENABLED, V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_DISABLED, V4L2_MPEG_VIDEO_HEVC_LOOP_FILTER_MODE_DISABLED_AT_SLICE_BOUNDARY}; control(ids[0], settings.hevc ? hevc_modes[filter[0]] : modes[filter[0]]); control(ids[1], filter[1]); control(ids[2], filter[2]); trace("encoder deblocking mode=%d alpha=%d beta=%d via V4L2", filter[0], filter[1], filter[2]); } void queue_capture(unsigned index) { QueueBuffer q(CAPTURE, index); q.plane.length = capture[index]->size; checked(fd, VIDIOC_QBUF, &q.buffer, "encoder QBUF CAPTURE"); } void stop(VAStatus status) noexcept { if (fd >= 0) { unsigned type = OUTPUT; if (output_on) call(fd, VIDIOC_STREAMOFF, &type); output_on = false; type = CAPTURE; if (capture_on) call(fd, VIDIOC_STREAMOFF, &type); capture_on = false; } failed = true; // STREAMOFF must precede release of imported input storage. for (auto &[token, job] : jobs) { job->task->status = status; job->task->pending = false; job->output->coded_ready = false; } jobs.clear(); for (auto &slot : slots) slot.reset(); } void refresh() { if (jobs.empty()) return; try { for (;;) { QueueBuffer q(OUTPUT, 0, import); if (call(fd, VIDIOC_DQBUF, &q.buffer) < 0) { check(errno == EAGAIN, "encoder DQBUF OUTPUT failed", VA_STATUS_ERROR_ENCODING_ERROR); break; } check(q.buffer.index < slots.size() && slots[q.buffer.index] && !slots[q.buffer.index]->input_done && !(q.buffer.flags & V4L2_BUF_FLAG_ERROR), "invalid encoder input completion", VA_STATUS_ERROR_ENCODING_ERROR); slots[q.buffer.index]->input_done = true; // OUTPUT DQBUF returns ownership of the raw slot even if the // corresponding coded output is still in the hardware pipeline. slots[q.buffer.index].reset(); } for (;;) { QueueBuffer q(CAPTURE); if (call(fd, VIDIOC_DQBUF, &q.buffer) < 0) { check(errno == EAGAIN, "encoder DQBUF CAPTURE failed", VA_STATUS_ERROR_ENCODING_ERROR); break; } check(q.buffer.index < capture.size() && !(q.buffer.flags & V4L2_BUF_FLAG_ERROR), "encoder CAPTURE error", VA_STATUS_ERROR_ENCODING_ERROR); const auto &memory = capture[q.buffer.index]; check(q.plane.data_offset <= q.plane.bytesused && q.plane.bytesused <= memory->size, "invalid encoder bitstream range", VA_STATUS_ERROR_ENCODING_ERROR); unsigned size = q.plane.bytesused - q.plane.data_offset; if (size) { uint64_t token = uint64_t(q.buffer.timestamp.tv_sec) * 1000000 + q.buffer.timestamp.tv_usec; auto it = jobs.find(token); check(it != jobs.end() && !it->second->frame_done, "encoder timestamp mismatch", VA_STATUS_ERROR_ENCODING_ERROR); auto &job = *it->second; auto &output = *job.output; if (size > output.data.size()) { output.coded.status = VA_CODED_BUF_STATUS_SLICE_OVERFLOW_MASK; throw Error(VA_STATUS_ERROR_NOT_ENOUGH_BUFFER, "coded buffer too small"); } std::memcpy(output.data.data(), memory->map() + q.plane.data_offset, size); output.coded.size = size; output.coded.buf = output.data.data(); job.frame_done = true; } queue_capture(q.buffer.index); } for (auto it = jobs.begin(); it != jobs.end();) { auto &job = *it->second; if (job.input_done && job.frame_done) { job.output->coded_ready = true; job.task->pending = false; trace("encoded token=%llu bytes=%u", (unsigned long long)it->first, job.output->coded.size); it = jobs.erase(it); } else { ++it; } } } catch (const Error &error) { stop(error.status); throw; } catch (...) { stop(VA_STATUS_ERROR_ENCODING_ERROR); throw; } } bool wait(const std::shared_ptr &task, uint64_t timeout) { const auto start = std::chrono::steady_clock::now(); // Bound active waits, not time since submission: an idle client can // leave CAPTURE full until its next VA call requeues those buffers. // Also avoid overflowing time_point for VA_TIMEOUT_INFINITE. const auto end = start + std::chrono::nanoseconds(std::min(timeout, 5000000000)); while (task->pending) { refresh(); if (!task->pending) break; const auto now = std::chrono::steady_clock::now(); if (now >= end) { if (timeout <= 5000000000) return false; // Caller timeout does not cancel or poison work. stop(VA_STATUS_ERROR_ENCODING_ERROR); throw Error(VA_STATUS_ERROR_ENCODING_ERROR, "encoder timed out"); } bool input_pending = false; for (const auto &[token, job] : jobs) input_pending |= !job->input_done; auto remaining = std::chrono::duration_cast(end - now); pollfd p{fd, short(POLLIN | (input_pending ? POLLOUT : 0)), 0}; int ret = poll(&p, 1, std::min(100, remaining.count() + 1)); if ((ret < 0 && errno != EINTR) || (ret > 0 && (p.revents & (POLLERR | POLLHUP | POLLNVAL)))) { stop(VA_STATUS_ERROR_ENCODING_ERROR); throw Error(VA_STATUS_ERROR_ENCODING_ERROR, "encoder poll failed"); } } check(task->status == VA_STATUS_SUCCESS, "asynchronous encoding failed", task->status); return true; } void finish() { while (!jobs.empty()) { auto task = jobs.begin()->second->task; check(wait(task, VA_TIMEOUT_INFINITE), "encoder timed out", VA_STATUS_ERROR_ENCODING_ERROR); } } }; Encoder::Encoder(const std::string &device, unsigned width, unsigned height, VAProfile profile, const EncodeSettings &s, const EncodePicture &p, int render_fd) { // VA contexts may use macroblock-aligned dimensions (FFmpeg does this). // The sequence crop determines the visible dimensions sent to Iris. if (!s.hevc) { const auto &seq = s.sequence; unsigned coded_width = unsigned(seq.picture_width_in_mbs) * 16; unsigned coded_height = unsigned(seq.picture_height_in_mbs) * 16; check(s.has_sequence && coded_width >= width && coded_width - width < 16 && coded_height >= height && coded_height - height < 16 && uint64_t(seq.frame_crop_right_offset) * 2 < coded_width && uint64_t(seq.frame_crop_bottom_offset) * 2 < coded_height, "invalid encoder sequence dimensions", VA_STATUS_ERROR_INVALID_PARAMETER); if (seq.frame_cropping_flag) { coded_width -= seq.frame_crop_right_offset * 2; coded_height -= seq.frame_crop_bottom_offset * 2; } check(coded_width <= width && coded_height <= height && coded_width >= 128 && coded_height >= 128, "encode crop exceeds context", VA_STATUS_ERROR_INVALID_PARAMETER); width = coded_width; height = coded_height; } check(width >= 128 && height >= 128 && width <= 3840 && height <= 2160 && !(width & 1) && !(height & 1), "unsupported visible encoder dimensions", VA_STATUS_ERROR_RESOLUTION_NOT_SUPPORTED); validate(s, p, width, height); auto impl = std::make_unique( width, height, s, s.hevc ? 1 : p.params.pic_fields.bits.entropy_coding_mode_flag); auto &e = *impl; e.filter = deblocking(p, s.hevc); e.render_fd = fcntl(render_fd, F_DUPFD_CLOEXEC, 0); check(e.render_fd >= 0, "duplicate encoder DRM fd"); int coherent = 0; int probe = irisva_msm_allocate(e.render_fd, 4096, &coherent); check(probe >= 0, "probe encoder shared memory", VA_STATUS_ERROR_ALLOCATION_FAILED); close(probe); e.import = coherent; e.fd = open(device.c_str(), O_RDWR | O_NONBLOCK | O_CLOEXEC); check(e.fd >= 0, "open Iris encoder"); v4l2_format fmt{}; fmt.type = CAPTURE; fmt.fmt.pix_mp.width = width; fmt.fmt.pix_mp.height = height; fmt.fmt.pix_mp.pixelformat = s.hevc ? V4L2_PIX_FMT_HEVC : V4L2_PIX_FMT_H264; checked(e.fd, VIDIOC_S_FMT, &fmt, "encoder S_FMT coded format"); check(fmt.fmt.pix_mp.pixelformat == (s.hevc ? V4L2_PIX_FMT_HEVC : V4L2_PIX_FMT_H264), "Iris does not support the requested encode format", VA_STATUS_ERROR_UNSUPPORTED_PROFILE); fmt = {}; fmt.type = OUTPUT; fmt.fmt.pix_mp.width = width; fmt.fmt.pix_mp.height = height; fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_NV12; fmt.fmt.pix_mp.field = V4L2_FIELD_NONE; checked(e.fd, VIDIOC_S_FMT, &fmt, "encoder S_FMT NV12"); check(fmt.fmt.pix_mp.pixelformat == V4L2_PIX_FMT_NV12 && fmt.fmt.pix_mp.num_planes == 1, "unsupported Iris encoder input layout"); e.stride = fmt.fmt.pix_mp.plane_fmt[0].bytesperline; e.storage_height = fmt.fmt.pix_mp.height; e.input_size = fmt.fmt.pix_mp.plane_fmt[0].sizeimage; v4l2_selection crop{}; crop.type = V4L2_BUF_TYPE_VIDEO_OUTPUT; crop.target = V4L2_SEL_TGT_CROP; crop.r.width = width; crop.r.height = height; checked(e.fd, VIDIOC_S_SELECTION, &crop, "encoder S_SELECTION"); if (s.hevc) { e.control(V4L2_CID_MPEG_VIDEO_HEVC_PROFILE, V4L2_MPEG_VIDEO_HEVC_PROFILE_MAIN); e.control(V4L2_CID_MPEG_VIDEO_HEVC_LEVEL, hevc_level_control(s.hevc_sequence.general_level_idc)); v4l2_queryctrl tier{}; tier.id = V4L2_CID_MPEG_VIDEO_HEVC_TIER; int ret = call(e.fd, VIDIOC_QUERYCTRL, &tier); check(ret == 0 || errno == EINVAL, "query HEVC tier control", VA_STATUS_ERROR_ENCODING_ERROR); if (ret == 0 && !(tier.flags & V4L2_CTRL_FLAG_DISABLED)) e.control(tier.id, s.hevc_sequence.general_tier_flag); else check(!s.hevc_sequence.general_tier_flag, "HEVC High tier requires Iris tier control", VA_STATUS_ERROR_UNIMPLEMENTED); } else { e.control(V4L2_CID_MPEG_VIDEO_H264_PROFILE, profile == VAProfileH264High ? V4L2_MPEG_VIDEO_H264_PROFILE_HIGH : profile == VAProfileH264Main ? V4L2_MPEG_VIDEO_H264_PROFILE_MAIN : V4L2_MPEG_VIDEO_H264_PROFILE_CONSTRAINED_BASELINE); e.control(V4L2_CID_MPEG_VIDEO_H264_LEVEL, level_control(s.sequence.level_idc)); e.control(V4L2_CID_MPEG_VIDEO_H264_ENTROPY_MODE, e.entropy); } e.control(V4L2_CID_MPEG_VIDEO_B_FRAMES, 0); // Keyframes follow VA picture requests; prevent autonomous periodic IDRs. e.control(V4L2_CID_MPEG_VIDEO_GOP_SIZE, std::numeric_limits::max()); e.control(V4L2_CID_MPEG_VIDEO_HEADER_MODE, V4L2_MPEG_VIDEO_HEADER_MODE_JOINED_WITH_1ST_FRAME); e.control(V4L2_CID_MPEG_VIDEO_PREPEND_SPSPPS_TO_IDR, 1); e.control(V4L2_CID_MPEG_VIDEO_FRAME_RC_ENABLE, s.rate_control != VA_RC_CQP); if (s.rate_control != VA_RC_CQP) { e.control(V4L2_CID_MPEG_VIDEO_BITRATE_MODE, s.rate_control == VA_RC_CBR ? V4L2_MPEG_VIDEO_BITRATE_MODE_CBR : V4L2_MPEG_VIDEO_BITRATE_MODE_VBR); e.control(V4L2_CID_MPEG_VIDEO_BITRATE, s.bitrate); e.control(V4L2_CID_MPEG_VIDEO_BITRATE_PEAK, s.peak_bitrate); } e.control(s.hevc ? V4L2_CID_MPEG_VIDEO_HEVC_MIN_QP : V4L2_CID_MPEG_VIDEO_H264_MIN_QP, s.min_qp); e.control(s.hevc ? V4L2_CID_MPEG_VIDEO_HEVC_MAX_QP : V4L2_CID_MPEG_VIDEO_H264_MAX_QP, s.max_qp); e.configure_deblocking(); for (unsigned type : {CAPTURE, OUTPUT}) { v4l2_streamparm parm{}; parm.type = type; parm.parm.output.timeperframe.numerator = s.fps_den; parm.parm.output.timeperframe.denominator = s.fps_num; checked(e.fd, VIDIOC_S_PARM, &parm, "encoder S_PARM"); } // Iris sizes coded buffers differently when rate control is disabled. // Refresh CAPTURE after controls, before allocating either queue. fmt = {}; fmt.type = CAPTURE; fmt.fmt.pix_mp.width = width; fmt.fmt.pix_mp.height = height; fmt.fmt.pix_mp.pixelformat = s.hevc ? V4L2_PIX_FMT_HEVC : V4L2_PIX_FMT_H264; checked(e.fd, VIDIOC_S_FMT, &fmt, "encoder refresh CAPTURE format"); for (unsigned type : {OUTPUT, CAPTURE}) { v4l2_requestbuffers req{}; req.type = type; req.memory = type == OUTPUT && e.import ? V4L2_MEMORY_DMABUF : V4L2_MEMORY_MMAP; req.count = 4; checked(e.fd, VIDIOC_REQBUFS, &req, "encoder REQBUFS"); check(req.count && req.count <= 64, "invalid encoder buffer count"); if (type == OUTPUT) { e.input_count = req.count; if (e.import) continue; } for (unsigned i = 0; i < req.count; ++i) { QueueBuffer q(type, i); checked(e.fd, VIDIOC_QUERYBUF, &q.buffer, "encoder QUERYBUF"); auto m = std::make_shared(); m->origin = MemoryOrigin::Iris; m->size = q.plane.length; void *ptr = mmap(nullptr, m->size, PROT_READ | PROT_WRITE, MAP_SHARED, e.fd, q.plane.m.mem_offset); check(ptr != MAP_FAILED, "mmap encoder buffer"); m->mapping = ptr; if (type == OUTPUT) { m->stride = e.stride; m->storage_height = e.storage_height; m->width = e.width; m->height = e.height; v4l2_exportbuffer exp{}; exp.type = OUTPUT; exp.index = i; exp.flags = O_CLOEXEC | O_RDWR; checked(e.fd, VIDIOC_EXPBUF, &exp, "encoder EXPBUF OUTPUT"); m->fd = exp.fd; e.raw.push_back(m); } else { e.capture.push_back(m); e.queue_capture(i); } } } e.slots.resize(e.input_count); e.staging.resize(e.input_count); unsigned type = OUTPUT; checked(e.fd, VIDIOC_STREAMON, &type, "encoder STREAMON OUTPUT"); e.output_on = true; type = CAPTURE; checked(e.fd, VIDIOC_STREAMON, &type, "encoder STREAMON CAPTURE"); e.capture_on = true; trace("encoder %s %ux%u stride=%u storage-height=%u size=%u RC=%u queue=%s", device.c_str(), width, height, e.stride, e.storage_height, e.input_size, s.rate_control, e.import ? "dmabuf" : "mmap"); impl_ = std::move(impl); } Encoder::~Encoder() = default; std::shared_ptr Encoder::encode(const EncodeSettings &s, const EncodePicture &p, const std::shared_ptr &input, const std::shared_ptr &coded) { auto &output = *coded; auto &e = *impl_; check(!e.failed, "encoder session failed; create a new context", VA_STATUS_ERROR_ENCODING_ERROR); validate(s, p, e.width, e.height); check(deblocking(p, s.hevc) == e.filter, "changing deblocking requires a new context", VA_STATUS_ERROR_UNIMPLEMENTED); check((s.hevc ? s.hevc_sequence.general_level_idc == e.settings.hevc_sequence.general_level_idc && s.hevc_sequence.general_tier_flag == e.settings.hevc_sequence.general_tier_flag : s.sequence.level_idc == e.settings.sequence.level_idc && p.params.pic_fields.bits.entropy_coding_mode_flag == e.entropy) && s.fps_num == e.settings.fps_num && s.fps_den == e.settings.fps_den && s.bitrate == e.settings.bitrate && s.peak_bitrate == e.settings.peak_bitrate && s.min_qp == e.settings.min_qp && s.max_qp == e.settings.max_qp, "changing encoder settings requires a new context", VA_STATUS_ERROR_UNIMPLEMENTED); const bool idr = s.hevc ? p.hevc_params.pic_fields.bits.idr_pic_flag : p.params.pic_fields.bits.idr_pic_flag; const bool intra = s.hevc ? p.hevc_slices.front().slice_type == 2 : p.slices.front().slice_type % 5 == 2; check(e.token || idr, "encoder must start with IDR", VA_STATUS_ERROR_INVALID_PARAMETER); // Clients with multiple GOPs per IDR (including Sunshine) submit non-IDR // I pictures / HEVC CRA. Iris only exposes FORCE_KEY_FRAME, which emits IDR. // Promote these requests to IDR and drain before changing the control below. // This is safe with our I/P-only, immediately preceding reference contract: // no later picture can refer across this reset. Firmware owns headers/POC; // never relabel an inter-coded picture or rewrite the firmware's NAL header. if (!intra) { VASurfaceID reference; bool valid; if (s.hevc) { const auto &ref = p.hevc_slices.front().ref_pic_list0[0]; reference = ref.picture_id; valid = !(ref.flags & (VA_PICTURE_HEVC_INVALID | VA_PICTURE_HEVC_LONG_TERM_REFERENCE)); } else { const auto &ref = p.slices.front().RefPicList0[0]; reference = ref.picture_id; valid = !(ref.flags & (VA_PICTURE_H264_INVALID | VA_PICTURE_H264_LONG_TERM_REFERENCE)); } check(valid && reference == e.last_reference && e.last_reference != VA_INVALID_SURFACE, "encoder only supports the immediately preceding reference", VA_STATUS_ERROR_UNIMPLEMENTED); } check(s.hevc ? p.hevc_params.pic_fields.bits.reference_pic_flag : p.params.pic_fields.bits.reference_pic_flag, "non-reference encoding unsupported", VA_STATUS_ERROR_UNIMPLEMENTED); auto memory = input->memory; check(memory && memory->fourcc == VA_FOURCC_NV12 && memory->width >= e.width && memory->height >= e.height, "invalid encoder input surface", VA_STATUS_ERROR_INVALID_SURFACE); check(!output.coded_mapped, "coded buffer is mapped", VA_STATUS_ERROR_SURFACE_BUSY); int qp = s.hevc ? int(p.hevc_params.pic_init_qp) + p.hevc_slices.front().slice_qp_delta : int(p.params.pic_init_qp) + p.slices.front().slice_qp_delta; check(qp >= 1 && qp <= 51, "invalid encode picture QP", VA_STATUS_ERROR_INVALID_PARAMETER); e.refresh(); check(!output.encode_task || !output.encode_task->pending, "coded buffer is pending", VA_STATUS_ERROR_SURFACE_BUSY); try { // Scalar V4L2 controls have no per-request association. Drain before a // QP change or forced IDR so they cannot affect an earlier queued frame. if ((s.rate_control == VA_RC_CQP && qp != e.current_qp) || (intra && e.token)) e.finish(); if (e.jobs.size() == e.input_count + e.capture.size()) { auto task = e.jobs.begin()->second->task; check(e.wait(task, VA_TIMEOUT_INFINITE), "encoder timed out", VA_STATUS_ERROR_ENCODING_ERROR); } unsigned index; const auto slot_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); for (;;) { for (index = 0; index < e.slots.size() && e.slots[index]; ++index) { } if (index < e.slots.size()) break; check(std::chrono::steady_clock::now() < slot_deadline, "encoder input queue timed out", VA_STATUS_ERROR_ENCODING_ERROR); pollfd ready{e.fd, short(POLLIN | POLLOUT), 0}; int ret = poll(&ready, 1, 100); check((ret >= 0 || errno == EINTR) && !(ready.revents & (POLLERR | POLLHUP | POLLNVAL)), "encoder input queue poll failed", VA_STATUS_ERROR_ENCODING_ERROR); e.refresh(); } const bool direct = e.import && memory->fd >= 0 && (memory->origin == MemoryOrigin::MsmCoherent || memory->origin == MemoryOrigin::Iris || memory->origin == MemoryOrigin::DmaHeap) && memory->stride == e.stride && memory->storage_height == e.storage_height && !memory->data_offset && memory->size >= e.input_size; if (!e.import) { auto raw = e.raw[index]; copy_surface(*raw, *memory, e.width, e.height); memory = raw; } else if (!direct) { // Never send write-combined GEM directly to a coherent VPU, or // GPU-write an Iris MMAP allocation with cached CPU aliases. if (!e.staging[index]) { auto m = std::make_shared(); m->width = e.width; m->height = e.height; m->stride = e.stride; m->storage_height = e.storage_height; m->size = e.input_size; int coherent = 0; m->fd = irisva_msm_allocate(e.render_fd, m->size, &coherent); check(m->fd >= 0 && coherent, "allocate coherent encoder staging", VA_STATUS_ERROR_ALLOCATION_FAILED); m->origin = MemoryOrigin::MsmCoherent; e.staging[index] = m; if (!e.copier) e.copier = std::make_unique(e.render_fd); } e.copier->copy(e.staging[index], memory, e.width, e.height); memory = e.staging[index]; } // Retain the input through both DQBUFs, and STREAMOFF before releasing // it if a job fails. Cache coherency does not replace lifetime/fences. wait_surface_access(*memory, POLLIN); trace("encoder input path=%s origin=%u", direct ? "direct" : e.import ? "staging" : "cpu-mmap", unsigned(memory->origin)); if (s.rate_control == VA_RC_CQP && qp != e.current_qp) { e.control(s.hevc ? V4L2_CID_MPEG_VIDEO_HEVC_I_FRAME_QP : V4L2_CID_MPEG_VIDEO_H264_I_FRAME_QP, qp); e.control(s.hevc ? V4L2_CID_MPEG_VIDEO_HEVC_P_FRAME_QP : V4L2_CID_MPEG_VIDEO_H264_P_FRAME_QP, qp); e.current_qp = qp; } if (intra && e.token) e.control(V4L2_CID_MPEG_VIDEO_FORCE_KEY_FRAME, 0); QueueBuffer q(OUTPUT, index, e.import); if (e.import) q.plane.m.fd = memory->fd; q.plane.length = memory->size; q.plane.bytesused = e.input_size; const uint64_t token = e.token + 1; q.buffer.timestamp.tv_sec = token / 1000000; q.buffer.timestamp.tv_usec = token % 1000000; auto job = std::make_shared(); job->task = std::make_shared(); job->output = coded; job->memory = memory; e.jobs.emplace(token, job); e.slots[index] = job; output.encode_task = job->task; output.coded_ready = false; output.coded = {}; checked(e.fd, VIDIOC_QBUF, &q.buffer, "encoder QBUF OUTPUT"); e.token = token; e.last_reference = p.reconstruction(s.hevc); trace("encoder queued token=%llu depth=%zu", (unsigned long long)token, e.jobs.size()); return job->task; } catch (const Error &error) { e.stop(error.status); throw; } catch (...) { e.stop(VA_STATUS_ERROR_ENCODING_ERROR); throw; } } bool Encoder::sync(const std::shared_ptr &task, uint64_t timeout) { return impl_->wait(task, timeout); } void Encoder::refresh() { impl_->refresh(); } void Encoder::finish() { impl_->finish(); } } // namespace irisva qcom-va-driver-1.1.0/src/gpu-copy.cpp000066400000000000000000000306471526764642600174370ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "internal.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace irisva { namespace { class GpuCopy { int fd_ = -1; gbm_device *gbm_ = nullptr; EGLDisplay display_ = EGL_NO_DISPLAY; EGLContext context_ = EGL_NO_CONTEXT; bool initialized_ = false; PFNEGLCREATEIMAGEKHRPROC create_image_ = nullptr; PFNEGLDESTROYIMAGEKHRPROC destroy_image_ = nullptr; PFNGLEGLIMAGETARGETTEXTURE2DOESPROC bind_image_ = nullptr; struct View { GpuCopy &owner; std::weak_ptr memory; unsigned width, height, stride, storage_height, offset, fourcc; EGLImageKHR images[2] = {EGL_NO_IMAGE_KHR, EGL_NO_IMAGE_KHR}; GLuint textures[2] = {}; unsigned columns[2] = {}, rows[2] = {}; View(GpuCopy &gpu, const std::shared_ptr &m, unsigned w, unsigned h) : owner(gpu), memory(m), width(w), height(h), stride(m->stride), storage_height(m->storage_height), offset(m->data_offset), fourcc(m->fourcc) {} ~View() { glDeleteTextures(2, textures); for (auto image : images) if (image != EGL_NO_IMAGE_KHR) owner.destroy_image_(owner.display_, image); } bool matches(const std::shared_ptr &m, unsigned w, unsigned h) const { return memory.lock() == m && width == w && height == h && stride == m->stride && storage_height == m->storage_height && offset == m->data_offset && fourcc == m->fourcc; } }; // Weak ownership is essential: retaining CAPTURE Memory would prevent requeue(). std::map> views_; void cleanup() noexcept { if (context_ != EGL_NO_CONTEXT) { glFinish(); views_.clear(); eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); eglDestroyContext(display_, context_); } if (initialized_) eglTerminate(display_); if (gbm_) gbm_device_destroy(gbm_); if (fd_ >= 0) close(fd_); eglReleaseThread(); } View &view(const std::shared_ptr &m, unsigned w, unsigned h) { auto it = views_.find(m.get()); if (it != views_.end()) { if (it->second->matches(m, w, h)) return *it->second; views_.erase(it); } check(w && h && w <= m->width && h <= m->height && h <= m->storage_height && m->fd >= 0 && m->stride <= INT_MAX, "GPU copy invalid buffer layout"); auto v = std::make_unique(*this, m, w, h); unsigned bytes = m->fourcc == VA_FOURCC_P010 ? 2 : 1; for (unsigned p = 0; p < 2; ++p) { // RGBA8 views transport four raw bytes per texel; no YUV conversion. uint64_t row_bytes = uint64_t(p ? ((w + 1) & ~1u) : w) * bytes; unsigned columns = (row_bytes + 3) / 4, rows = p ? (h + 1) / 2 : h; uint64_t offset = m->data_offset + (p ? uint64_t(m->stride) * m->storage_height : 0); check(uint64_t(columns) * 4 <= m->stride && offset <= INT_MAX && offset + uint64_t(rows - 1) * m->stride + uint64_t(columns) * 4 <= m->size, "GPU copy packed plane exceeds buffer"); EGLint attributes[] = {EGL_WIDTH, EGLint(columns), EGL_HEIGHT, EGLint(rows), EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_ABGR8888, EGL_DMA_BUF_PLANE0_FD_EXT, m->fd, EGL_DMA_BUF_PLANE0_OFFSET_EXT, EGLint(offset), EGL_DMA_BUF_PLANE0_PITCH_EXT, EGLint(m->stride), EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 0, EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, 0, EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE}; v->images[p] = create_image_(display_, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attributes); check(v->images[p] != EGL_NO_IMAGE_KHR, "GPU copy DMA-BUF import failed"); glGenTextures(1, &v->textures[p]); glBindTexture(GL_TEXTURE_2D, v->textures[p]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); bind_image_(GL_TEXTURE_2D, v->images[p]); check(glGetError() == GL_NO_ERROR, "GPU copy texture import failed"); v->columns[p] = columns; v->rows[p] = rows; } trace("GPU copy imported buffer=%p %ux%u offset=%u", static_cast(m.get()), w, h, m->data_offset); auto &result = *v; views_.emplace(m.get(), std::move(v)); return result; } public: explicit GpuCopy(int render_fd) { try { // Private GBM identity gives this engine its own EGLDisplay. Terminating // it must not terminate an application's shared surfaceless display. // Reuse the VA display's device, including inside a GPU sandbox. fd_ = fcntl(render_fd, F_DUPFD_CLOEXEC, 0); check(fd_ >= 0, "GPU copy duplicate VA DRM fd failed"); gbm_ = gbm_create_device(fd_); check(gbm_, "GPU copy GBM device failed"); auto get_display = reinterpret_cast( eglGetProcAddress("eglGetPlatformDisplayEXT")); create_image_ = reinterpret_cast(eglGetProcAddress("eglCreateImageKHR")); destroy_image_ = reinterpret_cast( eglGetProcAddress("eglDestroyImageKHR")); bind_image_ = reinterpret_cast( eglGetProcAddress("glEGLImageTargetTexture2DOES")); check(get_display && create_image_ && destroy_image_ && bind_image_, "GPU copy EGL entrypoints missing"); display_ = get_display(EGL_PLATFORM_GBM_KHR, gbm_, nullptr); check(display_ != EGL_NO_DISPLAY && eglInitialize(display_, nullptr, nullptr), "GPU copy EGL initialize failed"); initialized_ = true; check(eglBindAPI(EGL_OPENGL_ES_API), "GPU copy EGL bind API failed"); EGLint attributes[] = {EGL_CONTEXT_MAJOR_VERSION_KHR, 3, EGL_CONTEXT_MINOR_VERSION_KHR, 2, EGL_NONE}; context_ = eglCreateContext(display_, EGL_NO_CONFIG_KHR, EGL_NO_CONTEXT, attributes); check(context_ != EGL_NO_CONTEXT && eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, context_), "GPU copy GLES 3.2 context failed"); auto renderer = reinterpret_cast(glGetString(GL_RENDERER)); check(renderer && !strstr(renderer, "llvmpipe") && !strstr(renderer, "softpipe") && !strstr(renderer, "Software"), "GPU copy requires hardware rendering"); trace("GPU copy initialized renderer=%s (private GBM display, worker thread)", renderer); } catch (...) { cleanup(); throw; } } ~GpuCopy() { cleanup(); } void copy(const std::shared_ptr &dst, const std::shared_ptr &src, unsigned w, unsigned h) { check(dst != src && dst->fourcc == src->fourcc && (src->fourcc == VA_FOURCC_NV12 || src->fourcc == VA_FOURCC_P010), "GPU copy format mismatch", VA_STATUS_ERROR_INVALID_IMAGE_FORMAT); for (auto it = views_.begin(); it != views_.end();) { if (it->second->memory.expired()) it = views_.erase(it); else ++it; } wait_surface_access(*src, POLLIN); wait_surface_access(*dst, POLLOUT); auto &s = view(src, w, h), &d = view(dst, w, h); for (unsigned p = 0; p < 2; ++p) glCopyImageSubData(s.textures[p], GL_TEXTURE_2D, 0, 0, 0, 0, d.textures[p], GL_TEXTURE_2D, 0, 0, 0, 0, s.columns[p], s.rows[p], 1); // The persistent export consumer may render without another VA sync call. glFinish(); check(glGetError() == GL_NO_ERROR, "GPU copy failed; try IRIS_VAAPI_COPY=cpu"); } }; } // namespace struct SurfaceCopier::Impl { bool cpu; std::mutex mutex; std::condition_variable wake; bool stop = false; std::packaged_task task; std::thread worker; explicit Impl(int render_fd) { const char *mode = std::getenv("IRIS_VAAPI_COPY"); check(!mode || !strcmp(mode, "gpu") || !strcmp(mode, "cpu"), "IRIS_VAAPI_COPY must be gpu or cpu", VA_STATUS_ERROR_INVALID_PARAMETER); cpu = mode && !strcmp(mode, "cpu"); trace("surface copy selected mode=%s", cpu ? "cpu" : "gpu"); if (!cpu) worker = std::thread([this, render_fd] { // All EGL calls, including teardown, stay on this thread. VA entry // points may migrate between threads with arbitrary caller GL state. std::unique_ptr gpu; std::exception_ptr init_error; try { gpu = std::make_unique(render_fd); } catch (...) { init_error = std::current_exception(); } for (;;) { std::unique_lock lock(mutex); wake.wait(lock, [&] { return stop || task.valid(); }); if (stop) break; auto current = std::move(task); lock.unlock(); // The packaged task conveys errors without throwing out of the worker. current(gpu.get(), init_error); } }); } ~Impl() { if (worker.joinable()) { { std::lock_guard lock(mutex); stop = true; } wake.notify_one(); worker.join(); } } }; SurfaceCopier::SurfaceCopier(int render_fd) : render_fd_(render_fd) {} SurfaceCopier::~SurfaceCopier() = default; void SurfaceCopier::copy(const std::shared_ptr &dst, const std::shared_ptr &src, unsigned w, unsigned h) { try { if (!impl_) impl_ = std::make_unique(render_fd_); if (impl_->cpu) { copy_surface(*dst, *src, w, h); return; } auto start = std::chrono::steady_clock::now(); std::packaged_task task( [&](GpuCopy *gpu, std::exception_ptr error) { if (error) std::rethrow_exception(error); gpu->copy(dst, src, w, h); }); auto done = task.get_future(); { std::lock_guard lock(impl_->mutex); impl_->task = std::move(task); } impl_->wake.notify_one(); done.get(); trace("surface copy mode=gpu %ux%u fourcc=%#x capture=%u time-us=%lld", w, h, src->fourcc, src->index, (long long)std::chrono::duration_cast( std::chrono::steady_clock::now() - start) .count()); } catch (const Error &) { throw; } catch (const std::exception &e) { throw Error(VA_STATUS_ERROR_OPERATION_FAILED, std::string("surface copy: ") + e.what()); } } } // namespace irisva qcom-va-driver-1.1.0/src/h264.cpp000066400000000000000000000315471526764642600163570ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "internal.hpp" #include namespace irisva { namespace { class Bits { std::vector bytes_; unsigned position_ = 0; public: void bit(unsigned value) { if (!(position_ & 7)) bytes_.push_back(0); bytes_.back() |= (value & 1) << (7 - (position_++ & 7)); } void bits(uint32_t value, unsigned count) { for (unsigned i = count; i; --i) bit(value >> (i - 1)); } void ue(uint32_t value) { check(value != UINT32_MAX, "Exp-Golomb overflow"); unsigned n = 0; uint32_t code = value + 1; for (uint32_t v = code; v >>= 1;) ++n; for (unsigned i = 0; i < n; ++i) bit(0); bits(code, n + 1); } void se(int value) { ue(value <= 0 ? -2 * value : 2 * value - 1); } unsigned position() const { return position_; } void nal(std::vector &out, uint8_t header, bool trailing = true) { if (trailing) bit(1); while (position_ & 7) bit(0); out.insert(out.end(), {0, 0, 0, 1, header}); unsigned zeros = 0; for (uint8_t v : bytes_) { if (zeros == 2 && v <= 3) { out.push_back(3); zeros = 0; } out.push_back(v); zeros = v == 0 ? zeros + 1 : 0; } } }; class Reader { std::vector bytes_; size_t position_ = 0; public: explicit Reader(const std::vector &bytes) { unsigned zeros = 0; for (size_t i = 1; i < bytes.size(); ++i) { uint8_t v = bytes[i]; if (zeros == 2 && v == 3) { zeros = 0; continue; } bytes_.push_back(v); zeros = v == 0 ? zeros + 1 : 0; } } unsigned bit() { check(position_ < bytes_.size() * 8, "truncated H264 slice header", VA_STATUS_ERROR_INVALID_BUFFER); unsigned p = position_++; return (bytes_[p / 8] >> (7 - p % 8)) & 1; } unsigned position() const { return position_; } unsigned size_bits() const { return bytes_.size() * 8; } void seek(unsigned position) { check(position <= size_bits(), "H264 bit position out of bounds"); position_ = position; } uint32_t bits(unsigned count) { check(count <= 32, "invalid H264 bit field"); uint32_t v = 0; for (unsigned i = 0; i < count; ++i) v = (v << 1) | bit(); return v; } void copy(Bits &out, unsigned end) { check(end >= position_ && end <= size_bits(), "invalid H264 header offset", VA_STATUS_ERROR_INVALID_BUFFER); while (position_ < end) out.bit(bit()); } uint32_t ue() { unsigned n = 0; while (!bit()) check(++n < 31, "invalid H264 Exp-Golomb"); uint32_t v = 1; for (unsigned i = 0; i < n; ++i) v = (v << 1) | bit(); return v - 1; } }; // POC type 1's SPS offset cycle is absent from VA-API. Encode the resolved // picture order counts as type 0 instead, preserving every slice-data bit. // CABAC data stays byte aligned; CAVLC's rbsp_trailing_bits are regenerated. void rewrite_poc(const Slice &slice, const VAPictureParameterBufferH264 &p, std::vector &out) { Reader r(slice.bytes); r.ue(); r.ue(); r.ue(); r.bits(p.seq_fields.bits.log2_max_frame_num_minus4 + 4); bool field = false, bottom = false; if (!p.seq_fields.bits.frame_mbs_only_flag) { field = r.bit(); if (field) bottom = r.bit(); } if ((slice.bytes[0] & 31) == 5) r.ue(); unsigned poc_start = r.position(); if (!p.seq_fields.bits.delta_pic_order_always_zero_flag) { r.ue(); // signed Exp-Golomb occupies the same bits as unsigned if (p.pic_fields.bits.pic_order_present_flag && !field) r.ue(); } unsigned poc_end = r.position(); Bits rewritten; r.seek(0); r.copy(rewritten, poc_start); int poc = bottom ? p.CurrPic.BottomFieldOrderCnt : p.CurrPic.TopFieldOrderCnt; rewritten.bits(uint32_t(poc) & 65535, 16); if (!field) { int64_t delta = int64_t(p.CurrPic.BottomFieldOrderCnt) - p.CurrPic.TopFieldOrderCnt; check(delta > INT32_MIN / 2 && delta < INT32_MAX / 2, "H264 field POC delta out of range"); rewritten.se(int(delta)); } r.seek(poc_end); if (p.pic_fields.bits.entropy_coding_mode_flag) { check(slice.params.slice_data_bit_offset >= 8, "missing H264 slice header offset", VA_STATUS_ERROR_INVALID_BUFFER); unsigned end = slice.params.slice_data_bit_offset - 8; r.copy(rewritten, end); while (rewritten.position() & 7) rewritten.bit(1); r.seek((end + 7) & ~7u); r.copy(rewritten, r.size_bits()); rewritten.nal(out, slice.bytes[0], false); } else { unsigned end = r.size_bits(); // Locate rbsp_stop_one_bit. Trailing zero bytes are not slice data. do { check(end > poc_end, "H264 missing RBSP stop bit"); r.seek(--end); } while (!r.bit()); r.seek(poc_end); r.copy(rewritten, end); rewritten.nal(out, slice.bytes[0]); } } const unsigned zigzag4[] = {0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15}; const unsigned zigzag8[] = {0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63}; void matrix(Bits &b, const uint8_t *values, const unsigned *scan, unsigned n) { b.bit(1); int last = 8; for (unsigned j = 0; j < n; ++j) { int value = values[scan[j]]; check(value != 0, "invalid zero H264 scaling list entry", VA_STATUS_ERROR_INVALID_BUFFER); int delta = ((value - last + 128) & 255) - 128; b.se(delta); last = value; } } } // namespace std::vector h264_bitstream(VAProfile profile, const Picture &pic, H264State &state) { check(pic.has_params && !pic.slices.empty() && pic.pending_slices.empty(), "H264 picture missing parameters or slices", VA_STATUS_ERROR_INVALID_BUFFER); const auto &p = pic.params; const auto &s = p.seq_fields.bits; const auto &f = p.pic_fields.bits; check(s.chroma_format_idc == 1 && !p.bit_depth_luma_minus8 && !p.bit_depth_chroma_minus8, "only H264 8-bit 4:2:0 is supported", VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT); check(s.frame_mbs_only_flag, "Iris currently rejects interlaced H264 sequences", VA_STATUS_ERROR_UNIMPLEMENTED); check(s.log2_max_frame_num_minus4 <= 12 && s.pic_order_cnt_type <= 2 && (s.pic_order_cnt_type != 0 || s.log2_max_pic_order_cnt_lsb_minus4 <= 12) && p.num_ref_frames <= 16, "invalid H264 sequence parameters", VA_STATUS_ERROR_INVALID_BUFFER); const bool rewrite = s.pic_order_cnt_type == 1; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" check(!p.num_slice_groups_minus1, "H264 slice groups unsupported", VA_STATUS_ERROR_UNIMPLEMENTED); #pragma GCC diagnostic pop unsigned profile_idc = profile == VAProfileH264High ? 100 : profile == VAProfileH264Main ? 77 : 66; std::vector out; Bits sps; sps.bits(profile_idc, 8); sps.bits(profile_idc == 66 ? 0xc0 : 0, 8); sps.bits(51, 8); // A conservative decoding level, independent of absent original level_idc. sps.ue(0); if (profile_idc >= 100) { sps.ue(1); sps.ue(0); sps.ue(0); sps.bit(0); sps.bit(0); } sps.ue(s.log2_max_frame_num_minus4); sps.ue(rewrite ? 0 : s.pic_order_cnt_type); if (rewrite || !s.pic_order_cnt_type) sps.ue(rewrite ? 12 : s.log2_max_pic_order_cnt_lsb_minus4); sps.ue(p.num_ref_frames); sps.bit(s.gaps_in_frame_num_value_allowed_flag); sps.ue(p.picture_width_in_mbs_minus1); // VA height is expressed in frame macroblocks, SPS in map units. sps.ue((p.picture_height_in_mbs_minus1 + 1) / (2 - s.frame_mbs_only_flag) - 1); sps.bit(s.frame_mbs_only_flag); if (!s.frame_mbs_only_flag) sps.bit(s.mb_adaptive_frame_field_flag); sps.bit(s.direct_8x8_inference_flag); sps.bit(0); // VA owns presentation cropping; coded pixels are retained. sps.bit(0); // VUI is absent from VA parameters; use the standard inferred DPB limit. std::vector new_sps; sps.nal(new_sps, 0x67); if (new_sps != state.sps) { trace("H264 SPS profile=%u original-poc=%u rewritten-poc=%u", profile_idc, s.pic_order_cnt_type, rewrite ? 0 : s.pic_order_cnt_type); out.insert(out.end(), new_sps.begin(), new_sps.end()); state.sps = new_sps; } std::map> pps_defaults; for (const auto &slice : pic.slices) { check(!slice.bytes.empty() && ((slice.bytes[0] & 31) == 1 || (slice.bytes[0] & 31) == 5), "invalid H264 slice NAL", VA_STATUS_ERROR_INVALID_BUFFER); Reader r(slice.bytes); r.ue(); unsigned raw_type = r.ue(); unsigned id = r.ue(); check(raw_type <= 9, "invalid H264 slice type", VA_STATUS_ERROR_INVALID_BUFFER); unsigned type = raw_type % 5; check(type <= 2, "H264 SP/SI switching slices unsupported", VA_STATUS_ERROR_UNIMPLEMENTED); check(id <= 255, "invalid H264 PPS ID"); auto [it, inserted] = pps_defaults.try_emplace(id, -1, -1); (void)inserted; if (type == 2 || type == 4) continue; // I/SI provides no reference defaults. r.bits(s.log2_max_frame_num_minus4 + 4); bool field = false; if (!s.frame_mbs_only_flag) { field = r.bit(); if (field) r.bit(); } if ((slice.bytes[0] & 31) == 5) r.ue(); if (!s.pic_order_cnt_type) { r.bits(s.log2_max_pic_order_cnt_lsb_minus4 + 4); if (f.pic_order_present_flag && !field) r.ue(); } else if (s.pic_order_cnt_type == 1 && !s.delta_pic_order_always_zero_flag) { r.ue(); if (f.pic_order_present_flag && !field) r.ue(); } if (f.redundant_pic_cnt_present_flag) r.ue(); if (type == 1) r.bit(); // direct_spatial_mv_pred_flag if (r.bit()) continue; // Explicit slice overrides must not overwrite PPS defaults. auto merge = [](int &previous, unsigned count) { check(count <= 31 && (previous < 0 || unsigned(previous) == count), "inconsistent H264 PPS reference defaults", VA_STATUS_ERROR_INVALID_BUFFER); previous = count; }; merge(it->second.first, slice.params.num_ref_idx_l0_active_minus1); if (type == 1) merge(it->second.second, slice.params.num_ref_idx_l1_active_minus1); } for (const auto &[id, counts] : pps_defaults) { Bits pps; pps.ue(id); pps.ue(0); pps.bit(f.entropy_coding_mode_flag); pps.bit(rewrite || f.pic_order_present_flag); pps.ue(0); pps.ue(std::max(0, counts.first)); pps.ue(std::max(0, counts.second)); pps.bit(f.weighted_pred_flag); pps.bits(f.weighted_bipred_idc, 2); pps.se(p.pic_init_qp_minus26); pps.se(p.pic_init_qs_minus26); pps.se(p.chroma_qp_index_offset); pps.bit(f.deblocking_filter_control_present_flag); pps.bit(f.constrained_intra_pred_flag); pps.bit(f.redundant_pic_cnt_present_flag); if (profile_idc >= 100) { pps.bit(f.transform_8x8_mode_flag); pps.bit(pic.has_iq); if (pic.has_iq) { for (unsigned j = 0; j < 6; ++j) matrix(pps, pic.iq.ScalingList4x4[j], zigzag4, 16); if (f.transform_8x8_mode_flag) for (unsigned j = 0; j < 2; ++j) matrix(pps, pic.iq.ScalingList8x8[j], zigzag8, 64); } pps.se(p.second_chroma_qp_index_offset); } std::vector new_pps; pps.nal(new_pps, 0x68); if (new_pps != state.pps[id]) { out.insert(out.end(), new_pps.begin(), new_pps.end()); state.pps[id] = new_pps; } } for (const auto &slice : pic.slices) { if (rewrite) { rewrite_poc(slice, p, out); continue; } out.insert(out.end(), {0, 0, 0, 1}); out.insert(out.end(), slice.bytes.begin(), slice.bytes.end()); } return out; } } // namespace irisva qcom-va-driver-1.1.0/src/hevc.cpp000066400000000000000000000562721526764642600166230ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "internal.hpp" #include namespace irisva { namespace { constexpr VAStatus invalid = VA_STATUS_ERROR_INVALID_BUFFER; unsigned ceil_log2(unsigned n) { unsigned bits = 0; for (unsigned v = n ? n - 1 : 0; v; v >>= 1) ++bits; return bits; } class Bits { std::vector bytes_; size_t pos_ = 0; public: bool aligned() const { return !(pos_ & 7); } void append(const uint8_t *data, size_t size) { check(aligned(), "HEVC byte append not aligned", invalid); bytes_.insert(bytes_.end(), data, data + size); pos_ += size * 8; } void bit(unsigned value) { if (!(pos_ & 7)) bytes_.push_back(0); bytes_.back() |= (value & 1) << (7 - (pos_++ & 7)); } void bits(uint32_t value, unsigned count) { check(count <= 32, "HEVC bit field too wide", invalid); for (unsigned i = count; i; --i) bit(value >> (i - 1)); } void ue(uint32_t value) { check(value < UINT32_MAX, "HEVC Exp-Golomb overflow", invalid); unsigned n = 0; for (uint32_t v = value + 1; v >>= 1;) ++n; for (unsigned i = 0; i < n; ++i) bit(0); bits(value + 1, n + 1); } void se(int value) { int64_t code = value <= 0 ? -2ll * value : 2ll * value - 1; check(code < UINT32_MAX, "HEVC signed Exp-Golomb overflow", invalid); ue(unsigned(code)); } void align() { bit(1); while (pos_ & 7) bit(0); } void nal(std::vector &out, unsigned header, bool trailing = true) { if (trailing) align(); check(!(pos_ & 7), "HEVC NAL not byte aligned", invalid); out.insert(out.end(), {0, 0, 0, 1, uint8_t(header >> 8), uint8_t(header)}); unsigned zeros = 0; for (uint8_t v : bytes_) { if (zeros == 2 && v <= 3) { out.push_back(3); zeros = 0; } out.push_back(v); zeros = v == 0 ? zeros + 1 : 0; } } }; class Reader { std::vector bytes_; size_t pos_ = 0; public: explicit Reader(const std::vector &nal) { check(nal.size() >= 3, "truncated HEVC NAL", invalid); unsigned zeros = 0; for (size_t i = 2; i < nal.size(); ++i) { uint8_t v = nal[i]; if (zeros == 2 && v == 3) { zeros = 0; continue; } bytes_.push_back(v); zeros = v == 0 ? zeros + 1 : 0; } } size_t position() const { return pos_; } size_t size_bits() const { return bytes_.size() * 8; } void seek(size_t p) { check(p <= size_bits(), "HEVC header offset out of range", invalid); pos_ = p; } void skip(size_t n) { check(n <= size_bits() - pos_, "truncated HEVC header", invalid); pos_ += n; } unsigned bit() { check(pos_ < size_bits(), "truncated HEVC slice header", invalid); size_t p = pos_++; return (bytes_[p / 8] >> (7 - p % 8)) & 1; } uint32_t bits(unsigned n) { check(n <= 32, "HEVC read field too wide", invalid); uint32_t v = 0; while (n--) v = (v << 1) | bit(); return v; } uint32_t ue() { unsigned n = 0; while (!bit()) check(++n < 31, "invalid HEVC Exp-Golomb", invalid); return ((1u << n) - 1) + bits(n); } void copy(Bits &out, size_t end) { check(end >= pos_ && end <= size_bits(), "invalid HEVC copy range", invalid); // CABAC payload is already byte aligned. Preserve it in bulk rather // than spending CPU time rewriting each compressed-data bit. if (!(pos_ & 7) && out.aligned()) { size_t n = (end - pos_) / 8; if (n) { out.append(bytes_.data() + pos_ / 8, n); pos_ += n * 8; } } while (pos_ < end) out.bit(bit()); } }; void ptl(Bits &b, unsigned profile) { b.bits(0, 3); b.bits(profile, 5); b.bits(1u << (31 - profile), 32); b.bits(0xb, 4); // progressive, non-packed, frame-only b.bits(0, 32); b.bits(0, 12); b.bits(186, 8); // level 6.2 // VA omits temporal-layer PTL; allow every legal temporal_id (1..7). b.bits(0, 16); // six absent sub-layer PTLs + two reserved 2-bit fields } void ordering(Bits &b, const VAPictureParameterBufferHEVC &p) { b.bit(0); b.ue(p.sps_max_dec_pic_buffering_minus1); b.ue(p.sps_max_dec_pic_buffering_minus1); b.ue(0); } void scaling_list(Bits &b, const uint8_t *values, unsigned side, unsigned dc, bool large) { b.bit(1); // explicit coefficients int last = 8; if (large) { check(dc, "zero HEVC scaling DC", invalid); b.se(int(dc) - 8); last = dc; } // HEVC diagonal scan, beginning (0,0), (0,1), (1,0). for (unsigned sum = 0; sum <= 2 * (side - 1); ++sum) { for (unsigned x = 0; x < side; ++x) { if (sum < x || sum - x >= side) continue; unsigned y = sum - x; int value = values[y * side + x]; check(value, "zero HEVC scaling coefficient", invalid); b.se(((value - last + 128) & 255) - 128); last = value; } } } void matrices(Bits &b, const VAIQMatrixBufferHEVC &iq) { for (unsigned i = 0; i < 6; ++i) scaling_list(b, iq.ScalingList4x4[i], 4, 0, false); for (unsigned i = 0; i < 6; ++i) scaling_list(b, iq.ScalingList8x8[i], 8, 0, false); for (unsigned i = 0; i < 6; ++i) scaling_list(b, iq.ScalingList16x16[i], 8, iq.ScalingListDC16x16[i], true); for (unsigned i = 0; i < 2; ++i) scaling_list(b, iq.ScalingList32x32[i], 8, iq.ScalingListDC32x32[i], true); } struct Reference { unsigned index; int poc; bool used; }; struct References { std::vector before, after, long_term; std::vector list[2]; }; References references(const VAPictureParameterBufferHEVC &p) { References result; std::set pocs; for (unsigned i = 0; i < 15; ++i) { const auto &r = p.ReferenceFrames[i]; if (r.picture_id == VA_INVALID_SURFACE || (r.flags & VA_PICTURE_HEVC_INVALID)) continue; check(r.pic_order_cnt != p.CurrPic.pic_order_cnt && pocs.insert(r.pic_order_cnt).second, "invalid HEVC reference POC", invalid); bool lt = r.flags & VA_PICTURE_HEVC_LONG_TERM_REFERENCE; bool used = r.flags & (VA_PICTURE_HEVC_RPS_ST_CURR_BEFORE | VA_PICTURE_HEVC_RPS_ST_CURR_AFTER | VA_PICTURE_HEVC_RPS_LT_CURR); auto &list = lt ? result.long_term : r.pic_order_cnt < p.CurrPic.pic_order_cnt ? result.before : result.after; list.push_back({i, r.pic_order_cnt, used}); } std::sort(result.before.begin(), result.before.end(), [](auto a, auto b) { return a.poc > b.poc; }); std::sort(result.after.begin(), result.after.end(), [](auto a, auto b) { return a.poc < b.poc; }); std::sort(result.long_term.begin(), result.long_term.end(), [](auto a, auto b) { return a.poc > b.poc; }); for (unsigned l = 0; l < 2; ++l) { for (const auto &r : l ? result.after : result.before) if (r.used) result.list[l].push_back(r.index); for (const auto &r : l ? result.before : result.after) if (r.used) result.list[l].push_back(r.index); for (const auto &r : result.long_term) if (r.used) result.list[l].push_back(r.index); } return result; } void rps(Bits &b, const References &refs, const VAPictureParameterBufferHEVC &p) { b.bit(0); // short_term_ref_pic_set_sps_flag; SPS has zero RPS entries b.ue(refs.before.size()); b.ue(refs.after.size()); for (unsigned l = 0; l < 2; ++l) { int64_t previous = p.CurrPic.pic_order_cnt; for (const auto &r : l ? refs.after : refs.before) { int64_t delta = l ? int64_t(r.poc) - previous : previous - r.poc; check(delta > 0 && delta <= 32768, "HEVC short-term POC delta out of range", invalid); b.ue(unsigned(delta - 1)); b.bit(r.used); previous = r.poc; } } if (p.slice_parsing_fields.bits.long_term_ref_pics_present_flag) { b.ue(refs.long_term.size()); // no SPS long-term entries unsigned n = p.log2_max_pic_order_cnt_lsb_minus4 + 4, mask = (1u << n) - 1; int64_t current_msb = int64_t(p.CurrPic.pic_order_cnt) - (unsigned(p.CurrPic.pic_order_cnt) & mask); int64_t previous_cycle = 0; for (const auto &r : refs.long_term) { unsigned lsb = unsigned(r.poc) & mask; int64_t cycle = (current_msb - (int64_t(r.poc) - lsb)) / (1u << n); check(cycle >= previous_cycle && cycle < UINT32_MAX, "HEVC long-term MSB cycle unsupported", VA_STATUS_ERROR_UNIMPLEMENTED); b.bits(lsb, n); b.bit(r.used); b.bit(1); b.ue(unsigned(cycle - previous_cycle)); previous_cycle = cycle; } } else check(refs.long_term.empty(), "HEVC long-term references without SPS flag", invalid); } // Preserve the original header except its RPS and reference-list mapping. The // original SPS RPS definitions are absent from VA; explicit picture-local RPS // and list modifications express the same VA reference graph to the firmware. unsigned rewrite_slice(const HevcSlice &slice, const VAPictureParameterBufferHEVC &p, const References &refs, std::vector &out) { Reader r(slice.bytes); Bits b; unsigned header = unsigned(slice.bytes[0]) << 8 | slice.bytes[1]; unsigned type = (header >> 9) & 63; check(!(header & 0x8000) && !(header & 0x1f8) && (header & 7) && type <= 31, "invalid or multilayer HEVC slice NAL", invalid); const auto &f = p.slice_parsing_fields.bits; unsigned first = r.bit(); b.bit(first); // VA owns presentation/bumping. Every submitted VA picture needs a // completed surface, even when the original bitstream suppresses output. if (type >= 16 && type <= 23) { r.bit(); b.bit(0); } size_t copied = r.position(); unsigned pps_id = r.ue(); check(pps_id <= 63, "invalid HEVC PPS ID", invalid); bool dependent = false; if (!first) { if (f.dependent_slice_segments_enabled_flag) dependent = r.bit(); unsigned ctb = 1u << (p.log2_min_luma_coding_block_size_minus3 + 3 + p.log2_diff_max_min_luma_coding_block_size); unsigned count = ((p.pic_width_in_luma_samples + ctb - 1) / ctb) * ((p.pic_height_in_luma_samples + ctb - 1) / ctb); check(r.bits(ceil_log2(count)) == slice.params.slice_segment_address, "HEVC slice address mismatch", invalid); } auto copy_to = [&](size_t end) { size_t old = r.position(); r.seek(copied); r.copy(b, end); copied = end; r.seek(old); }; if (!dependent) { r.skip(p.num_extra_slice_header_bits); unsigned slice_type = r.ue(); check(slice_type <= 2 && slice_type == slice.params.LongSliceFlags.fields.slice_type, "HEVC slice type mismatch", invalid); if (f.output_flag_present_flag) { copy_to(r.position()); r.bit(); b.bit(1); copied = r.position(); } if (type != 19 && type != 20) { r.bits(p.log2_max_pic_order_cnt_lsb_minus4 + 4); copy_to(r.position()); bool from_sps = r.bit(); if (from_sps) { check(p.num_short_term_ref_pic_sets > 0, "HEVC missing SPS RPS", invalid); r.bits(ceil_log2(p.num_short_term_ref_pic_sets)); } else { size_t start = r.position(); bool predicted = p.num_short_term_ref_pic_sets && r.bit(); if (predicted) { check(p.st_rps_bits > 0, "HEVC predicted RPS bit count missing", invalid); r.seek(start); r.skip(p.st_rps_bits); } else { unsigned neg = r.ue(), pos = r.ue(); check(neg + pos <= 15, "invalid HEVC RPS size", invalid); for (unsigned i = 0; i < neg + pos; ++i) { r.ue(); r.bit(); } } } if (f.long_term_ref_pics_present_flag) { unsigned ns = p.num_long_term_ref_pic_sps ? r.ue() : 0; unsigned np = r.ue(); check(ns <= p.num_long_term_ref_pic_sps && ns + np <= 15, "invalid HEVC long-term RPS", invalid); for (unsigned i = 0; i < ns + np; ++i) { if (i < ns) r.bits(ceil_log2(p.num_long_term_ref_pic_sps)); else { r.bits(p.log2_max_pic_order_cnt_lsb_minus4 + 4); r.bit(); } if (r.bit()) r.ue(); } } copied = r.position(); rps(b, refs, p); if (f.sps_temporal_mvp_enabled_flag) r.bit(); } if (f.sample_adaptive_offset_enabled_flag) { r.bit(); r.bit(); } if (slice_type != 2) { unsigned counts[2] = {p.num_ref_idx_l0_default_active_minus1 + 1u, p.num_ref_idx_l1_default_active_minus1 + 1u}; if (r.bit()) { counts[0] = r.ue() + 1; if (slice_type == 0) counts[1] = r.ue() + 1; } unsigned lists = slice_type == 0 ? 2 : 1, total = refs.list[0].size(); check(total > 0 && total <= 15, "HEVC inter slice without reference pictures", invalid); copy_to(r.position()); for (unsigned l = 0; l < lists; ++l) { check(counts[l] <= 15, "invalid HEVC active reference count", invalid); if (f.lists_modification_present_flag && total > 1 && r.bit()) r.skip(counts[l] * ceil_log2(total)); if (total > 1) b.bit(1); // PPS always enables list modifications for (unsigned j = 0; j < counts[l]; ++j) { unsigned index = slice.params.RefPicList[l][j]; auto it = std::find(refs.list[l].begin(), refs.list[l].end(), index); check(it != refs.list[l].end(), "HEVC active reference missing from RPS", invalid); if (total > 1) b.bits(unsigned(it - refs.list[l].begin()), ceil_log2(total)); } } copied = r.position(); } } // VA's data offset includes the two-byte NAL header, excludes EPBs. check(slice.params.slice_data_byte_offset >= 3, "missing HEVC slice data offset", invalid); size_t data = size_t(slice.params.slice_data_byte_offset - 2) * 8; check(data <= r.size_bits() && data >= r.position(), "invalid HEVC slice data offset", invalid); size_t alignment = data, parsed = r.position(); do { check(alignment > parsed && data - alignment < 8, "invalid HEVC byte alignment", invalid); r.seek(--alignment); } while (!r.bit()); r.seek(copied); r.copy(b, alignment); b.align(); r.seek(data); r.copy(b, r.size_bits()); b.nal(out, header, false); return pps_id; } void cache(std::vector &out, std::vector &previous, Bits &b, unsigned type) { std::vector nal; b.nal(nal, (type << 9) | 1); if (previous != nal) { out.insert(out.end(), nal.begin(), nal.end()); previous = std::move(nal); } } } // namespace std::vector hevc_bitstream(VAProfile profile, const HevcPicture &pic, HevcState &state) { check(pic.has_params && !pic.slices.empty() && pic.pending_slices.empty(), "HEVC picture missing parameters or slices", invalid); check(pic.slices.front().bytes.size() >= 2, "truncated HEVC NAL", invalid); unsigned type = (pic.slices.front().bytes[0] >> 1) & 63; if (!state.started) { // Kodi can recreate the VA context mid-GOP when switching/refreshing a // stream. Iris consumes dependent pictures before the first IRAP without // producing CAPTURE buffers. Reject them before queuing anything, so the // caller can discard the pictures and advance to BLA/IDR/CRA instead of // filling its surface pool with requests which will never complete. check(type >= 16 && type <= 21, "HEVC context requires an initial random-access picture", VA_STATUS_ERROR_DECODING_ERROR); } // A first CRA/BLA has NoRaslOutputFlag set in the stateful decoder. Its // RASL pictures can still reference the preceding GOP and produce no output. // RADL pictures are independently decodable and must remain accepted. check(!(state.no_rasl_output && (type == 8 || type == 9)), "HEVC RASL picture unavailable after random access", VA_STATUS_ERROR_DECODING_ERROR); const auto &p = pic.params; const auto &f = p.pic_fields.bits; const auto &s = p.slice_parsing_fields.bits; unsigned depth = profile == VAProfileHEVCMain10 ? 2 : 0; check(f.chroma_format_idc == 1 && !f.separate_colour_plane_flag && p.bit_depth_luma_minus8 == depth && p.bit_depth_chroma_minus8 == depth, "HEVC requires matching Main NV12 or Main10 P010", VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT); check(!(p.CurrPic.flags & VA_PICTURE_HEVC_FIELD_PIC), "interlaced HEVC unsupported", VA_STATUS_ERROR_UNIMPLEMENTED); check(p.pic_width_in_luma_samples && p.pic_height_in_luma_samples && p.sps_max_dec_pic_buffering_minus1 <= 15 && p.log2_max_pic_order_cnt_lsb_minus4 <= 12 && p.num_short_term_ref_pic_sets <= 64 && p.num_long_term_ref_pic_sps <= 32 && p.log2_min_luma_coding_block_size_minus3 <= 3 && p.log2_diff_max_min_luma_coding_block_size <= 3 && p.log2_min_luma_coding_block_size_minus3 + p.log2_diff_max_min_luma_coding_block_size <= 3 && p.num_tile_columns_minus1 <= 19 && p.num_tile_rows_minus1 <= 21 && p.num_extra_slice_header_bits <= 7, "invalid HEVC sequence parameters", invalid); check(!f.scaling_list_enabled_flag || pic.has_iq, "HEVC scaling matrices missing", invalid); unsigned profile_id = depth ? 2 : 1; std::vector out, slices; std::set ids; auto refs = references(p); for (const auto &slice : pic.slices) ids.insert(rewrite_slice(slice, p, refs, slices)); Bits vps; vps.bits(0, 4); vps.bits(3, 2); vps.bits(0, 6); vps.bits(6, 3); vps.bit(1); vps.bits(65535, 16); ptl(vps, profile_id); ordering(vps, p); vps.bits(0, 6); vps.ue(0); vps.bit(0); vps.bit(0); cache(out, state.vps, vps, 32); Bits sps; sps.bits(0, 4); sps.bits(6, 3); sps.bit(1); ptl(sps, profile_id); sps.ue(0); sps.ue(1); sps.ue(p.pic_width_in_luma_samples); sps.ue(p.pic_height_in_luma_samples); sps.bit(0); sps.ue(depth); sps.ue(depth); sps.ue(p.log2_max_pic_order_cnt_lsb_minus4); ordering(sps, p); sps.ue(p.log2_min_luma_coding_block_size_minus3); sps.ue(p.log2_diff_max_min_luma_coding_block_size); sps.ue(p.log2_min_transform_block_size_minus2); sps.ue(p.log2_diff_max_min_transform_block_size); sps.ue(p.max_transform_hierarchy_depth_inter); sps.ue(p.max_transform_hierarchy_depth_intra); sps.bit(f.scaling_list_enabled_flag); if (f.scaling_list_enabled_flag) sps.bit(0); sps.bit(f.amp_enabled_flag); sps.bit(s.sample_adaptive_offset_enabled_flag); sps.bit(f.pcm_enabled_flag); if (f.pcm_enabled_flag) { sps.bits(p.pcm_sample_bit_depth_luma_minus1, 4); sps.bits(p.pcm_sample_bit_depth_chroma_minus1, 4); sps.ue(p.log2_min_pcm_luma_coding_block_size_minus3); sps.ue(p.log2_diff_max_min_pcm_luma_coding_block_size); sps.bit(f.pcm_loop_filter_disabled_flag); } sps.ue(0); sps.bit(s.long_term_ref_pics_present_flag); if (s.long_term_ref_pics_present_flag) sps.ue(0); sps.bit(s.sps_temporal_mvp_enabled_flag); sps.bit(f.strong_intra_smoothing_enabled_flag); sps.bit(0); sps.bit(0); // presentation metadata belongs to the VA application cache(out, state.sps, sps, 33); for (unsigned id : ids) { Bits pps; pps.ue(id); pps.ue(0); pps.bit(s.dependent_slice_segments_enabled_flag); pps.bit(s.output_flag_present_flag); pps.bits(p.num_extra_slice_header_bits, 3); pps.bit(f.sign_data_hiding_enabled_flag); pps.bit(s.cabac_init_present_flag); pps.ue(p.num_ref_idx_l0_default_active_minus1); pps.ue(p.num_ref_idx_l1_default_active_minus1); pps.se(p.init_qp_minus26); pps.bit(f.constrained_intra_pred_flag); pps.bit(f.transform_skip_enabled_flag); pps.bit(f.cu_qp_delta_enabled_flag); if (f.cu_qp_delta_enabled_flag) pps.ue(p.diff_cu_qp_delta_depth); pps.se(p.pps_cb_qp_offset); pps.se(p.pps_cr_qp_offset); pps.bit(s.pps_slice_chroma_qp_offsets_present_flag); pps.bit(f.weighted_pred_flag); pps.bit(f.weighted_bipred_flag); pps.bit(f.transquant_bypass_enabled_flag); pps.bit(f.tiles_enabled_flag); pps.bit(f.entropy_coding_sync_enabled_flag); if (f.tiles_enabled_flag) { pps.ue(p.num_tile_columns_minus1); pps.ue(p.num_tile_rows_minus1); pps.bit(0); for (unsigned i = 0; i < p.num_tile_columns_minus1; ++i) pps.ue(p.column_width_minus1[i]); for (unsigned i = 0; i < p.num_tile_rows_minus1; ++i) pps.ue(p.row_height_minus1[i]); pps.bit(f.loop_filter_across_tiles_enabled_flag); } pps.bit(f.pps_loop_filter_across_slices_enabled_flag); pps.bit(1); pps.bit(s.deblocking_filter_override_enabled_flag); pps.bit(s.pps_disable_deblocking_filter_flag); if (!s.pps_disable_deblocking_filter_flag) { pps.se(p.pps_beta_offset_div2); pps.se(p.pps_tc_offset_div2); } pps.bit(f.scaling_list_enabled_flag); if (f.scaling_list_enabled_flag) matrices(pps, pic.iq); pps.bit(1); pps.ue(p.log2_parallel_merge_level_minus2); pps.bit(s.slice_segment_header_extension_present_flag); pps.bit(0); cache(out, state.pps[id], pps, 34); } out.insert(out.end(), slices.begin(), slices.end()); // end_picture commits this candidate state only after Decoder::submit succeeds. if (type >= 16 && type <= 21) state.no_rasl_output = !state.started || type <= 20; state.started = true; return out; } } // namespace irisva qcom-va-driver-1.1.0/src/internal.hpp000066400000000000000000000305431526764642600175100ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern "C" int irisva_msm_allocate(int drm_fd, uint64_t size, int *coherent); extern "C" int irisva_msm_allocate_coherent(int drm_fd, uint64_t size); namespace irisva { inline bool is_rgb(unsigned fourcc) { return fourcc == VA_FOURCC_BGRA || fourcc == VA_FOURCC_RGBA; } struct Error : std::runtime_error { VAStatus status; Error(VAStatus s, const std::string &m) : std::runtime_error(m), status(s) {} }; inline void check(bool b, const char *m, VAStatus s = VA_STATUS_ERROR_OPERATION_FAILED) { if (!b) throw Error(s, m); } inline bool debug() { return std::getenv("IRIS_VAAPI_DEBUG") != nullptr; } template void trace(const char *f, A... a) { if (debug()) { std::fprintf(stderr, "iris-vaapi: "); std::fprintf(stderr, f, a...); std::fprintf(stderr, "\n"); } } enum class MemoryOrigin { Unknown, MsmWriteCombined, MsmCoherent, DmaHeap, Imported, Iris }; struct Memory { MemoryOrigin origin = MemoryOrigin::Unknown; int fd = -1; void *mapping = nullptr; size_t size = 0; unsigned index = 0, stride = 0, storage_height = 0, width = 0, height = 0; unsigned data_offset = 0, fourcc = VA_FOURCC_NV12; bool queued = false; std::shared_ptr vpp_registration; std::shared_ptr vulkan_registration; ~Memory(); uint8_t *map(); }; class Decoder; class SurfaceCopier { // Calls are serialized by the owning decoder or encoder, including lazy setup. struct Impl; // Borrowed VA display fd; duplicated when GPU copying is first used. [[maybe_unused]] int render_fd_; std::unique_ptr impl_; public: explicit SurfaceCopier(int render_fd); ~SurfaceCopier(); void copy(const std::shared_ptr &destination, const std::shared_ptr &source, unsigned width, unsigned height); }; class Encoder; struct EncodeTask { std::weak_ptr encoder; bool pending = true; VAStatus status = VA_STATUS_SUCCESS; }; struct Surface { std::shared_ptr encode_task; unsigned width, height, fourcc = VA_FOURCC_NV12; unsigned allocation_count = 1; std::shared_ptr memory; // An export before the first picture may be cached by the consumer for // this surface's whole lifetime (Chromium's NativePixmap frame pool). bool persistent_export = false; std::weak_ptr decoder; uint64_t token = 0; std::atomic status{VA_STATUS_SUCCESS}; std::atomic pending{false}; unsigned derived_images = 0, mapped_images = 0, acquired_handles = 0; }; struct Buffer { std::shared_ptr encode_task; VABufferType type; unsigned element_size = 0, elements = 0; std::vector data; std::shared_ptr parent; std::shared_ptr memory; int acquired_fd = -1; bool mapped = false; VACodedBufferSegment coded{}; bool coded_ready = false, coded_mapped = false; uint32_t context_id = VA_INVALID_ID; ~Buffer(); uint8_t *map(); void unmap(); }; struct Slice { VASliceParameterBufferH264 params{}; std::vector bytes; }; struct Picture { VAPictureParameterBufferH264 params{}; VAIQMatrixBufferH264 iq{}; bool has_params = false, has_iq = false; std::vector pending_slices; std::vector slices; }; struct H264State { std::vector sps; std::map> pps; }; struct HevcSlice { VASliceParameterBufferHEVC params{}; std::vector bytes; }; struct HevcPicture { VAPictureParameterBufferHEVC params{}; VAIQMatrixBufferHEVC iq{}; bool has_params = false, has_iq = false; std::vector pending_slices; std::vector slices; }; struct HevcState { bool started = false; bool no_rasl_output = false; std::vector vps, sps; std::map> pps; }; inline bool is_hevc(VAProfile p) { return p == VAProfileHEVCMain || p == VAProfileHEVCMain10; } inline bool is_vp9(VAProfile p) { return p == VAProfileVP9Profile0 || p == VAProfileVP9Profile2; } inline bool is_av1(VAProfile p) { return p == VAProfileAV1Profile0; } inline bool is_10bit(VAProfile p) { return p == VAProfileHEVCMain10 || p == VAProfileVP9Profile2; } struct Vp9Slice { VASliceParameterBufferVP9 params{}; std::vector bytes; }; struct Vp9Picture { VADecPictureParameterBufferVP9 params{}; bool has_params = false; std::vector pending_slices; std::vector slices; }; struct Av1Slice { VASliceParameterBufferAV1 params{}; std::vector bytes; }; struct Av1Picture { VADecPictureParameterBufferAV1 params{}; bool has_params = false; std::vector pending_slices; std::vector slices; }; struct Av1Reference { VASurfaceID surface = VA_INVALID_SURFACE; VADecPictureParameterBufferAV1 params{}; }; struct Av1State { std::vector sequence; std::array references; std::shared_ptr pending_reference; }; std::vector av1_bitstream(const Av1Picture &picture, Av1State &state, unsigned width, unsigned height); std::vector vp9_bitstream(VAProfile profile, const Vp9Picture &picture); std::vector h264_bitstream(VAProfile profile, const Picture &picture, H264State &state); std::vector hevc_bitstream(VAProfile profile, const HevcPicture &picture, HevcState &state); inline unsigned capture_pool_size(unsigned preferred, unsigned minimum, size_t buffer_size) { check(minimum && minimum <= 64 && buffer_size, "invalid CAPTURE requirements"); // Leave room in Iris's DMA address space for internal/reference buffers. // This bounds extra output storage, not total decoder memory usage. constexpr size_t budget = size_t{768} * 1024 * 1024; unsigned limit = std::min(size_t{64}, budget / buffer_size); check(limit >= minimum, "minimum CAPTURE pool exceeds memory budget", VA_STATUS_ERROR_ALLOCATION_FAILED); return std::min(std::max(preferred, minimum), limit); } class Decoder { int fd_ = -1; int render_fd_; // Borrowed from the VA display, which outlives the decoder. FILE *dump_ = nullptr; std::mutex mutex_; bool output_on_ = false, capture_on_ = false; bool source_change_ = false, last_ = false; unsigned width_, height_, preferred_pool_size_, fourcc_; VAStatus failure_ = VA_STATUS_SUCCESS; uint64_t next_token_ = 1; std::vector> output_, capture_; std::map> pending_; SurfaceCopier copier_; void allocate(unsigned type, unsigned count, std::vector> &pool); void configure_capture(); void pump(int timeout_ms); void requeue(); void recover_picture(uint64_t token); void fail(VAStatus status); public: Decoder(const std::string &device, unsigned width, unsigned height, unsigned surfaces, VAProfile profile, unsigned rt_format, int render_fd); ~Decoder(); void submit(const std::vector &bytes, const std::shared_ptr &surface); void sync(const std::shared_ptr &surface); void finish(); void refresh(); }; std::string find_device(bool &av1); std::string find_encoder_device(); struct EncodePicture { VAEncPictureParameterBufferH264 params{}; VAEncPictureParameterBufferHEVC hevc_params{}; bool has_params = false; std::vector slices; std::vector hevc_slices; VABufferID coded_buffer(bool hevc) const { return hevc ? hevc_params.coded_buf : params.coded_buf; } VASurfaceID reconstruction(bool hevc) const { return hevc ? hevc_params.decoded_curr_pic.picture_id : params.CurrPic.picture_id; } }; struct EncodeSettings { VAEncSequenceParameterBufferH264 sequence{}; VAEncSequenceParameterBufferHEVC hevc_sequence{}; bool hevc = false; bool has_sequence = false; unsigned rate_control = VA_RC_CQP; unsigned bitrate = 0, peak_bitrate = 0, min_qp = 1, max_qp = 51; unsigned fps_num = 30, fps_den = 1; }; void render_encode_buffer(EncodeSettings &settings, EncodePicture &picture, const Buffer &buffer); class Encoder { struct Impl; std::unique_ptr impl_; public: Encoder(const std::string &device, unsigned width, unsigned height, VAProfile profile, const EncodeSettings &settings, const EncodePicture &picture, int render_fd); ~Encoder(); std::shared_ptr encode(const EncodeSettings &settings, const EncodePicture &picture, const std::shared_ptr &input, const std::shared_ptr &output); bool sync(const std::shared_ptr &task, uint64_t timeout); void refresh(); void finish(); }; void copy_surface(Memory &destination, Memory &source, unsigned width, unsigned height); std::shared_ptr allocate_surface(int render_fd, unsigned width, unsigned height, unsigned fourcc); void fill_neutral_surface(Memory &memory, unsigned width, unsigned height); void wait_surface_access(const Memory &memory, short events); bool vpp_supported(VAProfile profile, VAEntrypoint entrypoint); void vpp_caps(VAProcPipelineCaps &caps, bool tone_mapping = false); void vpp_filter(const Buffer &buffer); struct VppPicture { std::shared_ptr source; VARectangle region{}, output{}; VABlendState blend{}; VAProcColorStandardType color_standard = VAProcColorStandardNone; VAProcColorProperties color_properties{}; unsigned rotation = VA_ROTATION_NONE; bool tone_mapping = false; float source_peak = 1000.0f; }; class Vpp { struct Impl; std::unique_ptr impl_; public: explicit Vpp(int render_fd); ~Vpp(); bool tone_mapping_supported(); void process(const std::vector &pictures, const std::shared_ptr &target); private: bool vulkan_available(); bool use_vulkan(bool overlay); void process_single(const VppPicture &picture, const std::shared_ptr &target); }; VppPicture vpp_picture(const Buffer &buffer, const std::shared_ptr &source, const Surface &target, const Buffer *filter, bool overlay); struct Context { VAProfile profile; VAProfile internal_profile; unsigned decode_surfaces = 0; unsigned decode_fourcc = VA_FOURCC_NV12; VAEntrypoint entrypoint = VAEntrypointVLD; unsigned width, height; std::shared_ptr decoder; std::shared_ptr encoder; std::unique_ptr vpp; std::vector vpp_pictures; bool vpp_failed = false; EncodeSettings encode_settings; EncodePicture encode_picture; std::shared_ptr target; Picture picture; H264State h264; HevcPicture hevc_picture; HevcState hevc; Vp9Picture vp9_picture; Av1Picture av1_picture; Av1State av1; }; struct Config { VAProfile profile; VAEntrypoint entrypoint; unsigned rate_control = VA_RC_CQP; }; struct Driver { std::recursive_mutex mutex; std::string device; bool av1 = false; std::string encoder_device; uint32_t next_id = 1; std::map configs; std::map> contexts; std::map> surfaces; std::map> buffers; std::map images; }; template auto lookup(std::map &table, uint32_t id, VAStatus status) { auto it = table.find(id); if (it == table.end()) throw Error(status, "invalid object ID"); return it->second; } } // namespace irisva qcom-va-driver-1.1.0/src/msm.c000066400000000000000000000033651526764642600161250ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Keep the MSM uAPI in C: older libdrm headers use the C++ keyword "or". #include #include #include #include #include #include // This uAPI flag may be missing from older libdrm headers. #ifndef MSM_BO_CACHED_COHERENT #define MSM_BO_CACHED_COHERENT 0x080000 #endif static int allocate(int drm_fd, uint64_t size, int *coherent, int allow_wc) { *coherent = 0; struct drm_msm_gem_new allocation = {.size = size, .flags = MSM_BO_CACHED_COHERENT}; if (drmIoctl(drm_fd, DRM_IOCTL_MSM_GEM_NEW, &allocation) < 0) { // Unsupported cache modes return EINVAL. Do not hide allocation or // permission failures by retrying with different memory attributes. if (!allow_wc || (errno != EINVAL && errno != EOPNOTSUPP)) return -1; allocation.flags = MSM_BO_WC; if (drmIoctl(drm_fd, DRM_IOCTL_MSM_GEM_NEW, &allocation) < 0) return -1; } int prime_fd = -1; int result = drmPrimeHandleToFD(drm_fd, allocation.handle, DRM_CLOEXEC | DRM_RDWR, &prime_fd); int saved_errno = errno; struct drm_gem_close close_handle = {.handle = allocation.handle}; if (drmIoctl(drm_fd, DRM_IOCTL_GEM_CLOSE, &close_handle) < 0) { if (prime_fd >= 0) close(prime_fd); return -1; } if (result < 0) { errno = saved_errno; return -1; } *coherent = allocation.flags == MSM_BO_CACHED_COHERENT; return prime_fd; } int irisva_msm_allocate(int drm_fd, uint64_t size, int *coherent) { return allocate(drm_fd, size, coherent, 1); } int irisva_msm_allocate_coherent(int drm_fd, uint64_t size) { int coherent; return allocate(drm_fd, size, &coherent, 0); } qcom-va-driver-1.1.0/src/overlay-params.h000066400000000000000000000060171526764642600202750ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef IRIS_VPP_OVERLAY_H #define IRIS_VPP_OVERLAY_H #include // Shared host/DSP ABI and Vulkan push constants. All offsets/strides are bytes. #define OVERLAY_P010 1 #define OVERLAY_BGRA 2 #define OVERLAY_PREMULTIPLIED 4 typedef struct { int32_t width, height, src_stride, src_y, src_uv, dst_stride, dst_y, dst_uv; int32_t rgb_stride, rgb_offset, rgb_width, rgb_height; // Destination rectangle, Q8 global alpha (256 = opaque), Q16 sampling steps. int32_t x, y, w, h, alpha, flags, step_x, step_y; // Q14 RGB-to-YUV coefficients and black level in eight-bit code values. int32_t matrix[9], black; } OverlayParams; static inline int overlay_plane_fits(uint64_t size, int offset, int stride, int width, int height) { return offset >= 0 && stride >= width && width > 0 && height > 0 && (uint64_t)offset + (uint64_t)(height - 1) * stride + width <= size; } static inline int overlay_valid(const OverlayParams *p, uint64_t src_size, uint64_t rgb_size, uint64_t dst_size) { int bytes = (p->flags & OVERLAY_P010) ? 2 : 1; if (p->width < 2 || p->width > 8192 || p->height < 2 || p->height > 8192 || ((p->width | p->height) & 1) || p->rgb_width < 1 || p->rgb_width > 8192 || p->rgb_height < 1 || p->rgb_height > 8192 || p->w < 1 || p->w > 8192 || p->h < 1 || p->h > 8192 || p->x < -32768 || p->x > 32767 || p->y < -32768 || p->y > 32767 || p->alpha < 0 || p->alpha > 256 || (p->flags & ~7) || ((p->src_stride | p->dst_stride | p->src_y | p->src_uv | p->dst_y | p->dst_uv) & 1) || ((p->rgb_stride | p->rgb_offset) & 3) || p->black < 0 || p->black > 16 || p->step_x != (p->rgb_width << 16) / p->w || p->step_y != (p->rgb_height << 16) / p->h) return 0; for (int i = 0; i < 9; ++i) if (p->matrix[i] < -16384 || p->matrix[i] > 16384) return 0; for (int row = 0; row < 3; ++row) { int magnitude = 0; for (int col = 0; col < 3; ++col) { int v = p->matrix[row * 3 + col]; magnitude += v < 0 ? -v : v; } if (magnitude > 32768) return 0; } // Separate Y/UV planes; permits an exactly matching base/output allocation. if ((uint64_t)p->src_y + (uint64_t)p->src_stride * p->height > (uint64_t)p->src_uv || (uint64_t)p->dst_y + (uint64_t)p->dst_stride * p->height > (uint64_t)p->dst_uv) return 0; return overlay_plane_fits(src_size, p->src_y, p->src_stride, p->width * bytes, p->height) && overlay_plane_fits(src_size, p->src_uv, p->src_stride, p->width * bytes, p->height / 2) && overlay_plane_fits(dst_size, p->dst_y, p->dst_stride, p->width * bytes, p->height) && overlay_plane_fits(dst_size, p->dst_uv, p->dst_stride, p->width * bytes, p->height / 2) && overlay_plane_fits(rgb_size, p->rgb_offset, p->rgb_stride, p->rgb_width * 4, p->rgb_height); } #endif qcom-va-driver-1.1.0/src/surface-copy.cpp000066400000000000000000000154671526764642600202770ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "internal.hpp" #include #include #include #include #include namespace irisva { void wait_surface_access(const Memory &memory, short events) { auto start = std::chrono::steady_clock::now(); pollfd fence{memory.fd, events, 0}; for (;;) { int result = poll(&fence, 1, 100); if (result < 0 && errno == EINTR) continue; check(result >= 0 && !(fence.revents & (POLLERR | POLLNVAL | POLLHUP)), "persistent surface DMA-BUF fence wait failed"); if (result && (fence.revents & fence.events)) break; check(std::chrono::steady_clock::now() - start < std::chrono::seconds(5), "persistent surface DMA-BUF fence timeout", VA_STATUS_ERROR_TIMEDOUT); } } namespace { int sync_ioctl(int fd, uint64_t flags) { dma_buf_sync sync{flags}; int result; do { result = ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync); } while (result < 0 && errno == EINTR); return result; } struct CpuAccess { Memory &memory; uint64_t access; bool active = false; CpuAccess(Memory &m, uint64_t flags) : memory(m), access(flags) { // DMA_BUF_IOCTL_SYNC maintains CPU cache coherency, not exclusion // against an outstanding GPU reader of a recycled exported surface. wait_surface_access(memory, flags & DMA_BUF_SYNC_WRITE ? POLLOUT : POLLIN); check(sync_ioctl(memory.fd, DMA_BUF_SYNC_START | access) == 0, "persistent surface DMA-BUF access failed"); active = true; } void finish() { if (!active) return; check(sync_ioctl(memory.fd, DMA_BUF_SYNC_END | access) == 0, "persistent surface DMA-BUF access end failed"); active = false; } ~CpuAccess() { if (active) sync_ioctl(memory.fd, DMA_BUF_SYNC_END | access); } }; void validate(const Memory &m, unsigned width, unsigned height, unsigned bytes) { const uint64_t chroma_offset = uint64_t(m.data_offset) + uint64_t(m.stride) * m.storage_height; const uint64_t chroma_rows = (height + 1) / 2; const uint64_t row_bytes = uint64_t((width + 1) & ~1u) * bytes; check(width && height && width <= m.width && height <= m.height && height <= m.storage_height && row_bytes <= m.stride && chroma_offset + (chroma_rows - 1) * m.stride + row_bytes <= m.size, "persistent surface copy exceeds buffer layout", VA_STATUS_ERROR_INVALID_SURFACE); } } // namespace std::shared_ptr allocate_surface(int render_fd, unsigned width, unsigned height, unsigned fourcc) { auto m = std::make_shared(); m->width = width; m->height = height; m->fourcc = fourcc; unsigned bytes_per_sample = is_rgb(fourcc) ? 4 : fourcc == VA_FOURCC_P010 ? 2 : 1; m->stride = (width * bytes_per_sample + 127) & ~127u; m->storage_height = (height + 31) & ~31u; long page_size = sysconf(_SC_PAGESIZE); check(page_size > 0, "cannot determine page size"); size_t size = size_t(m->stride) * m->storage_height; if (!is_rgb(fourcc)) size = size * 3 / 2; m->size = (size + size_t(page_size) - 1) / size_t(page_size) * size_t(page_size); int coherent = 0; m->fd = irisva_msm_allocate(render_fd, m->size, &coherent); m->origin = coherent ? MemoryOrigin::MsmCoherent : MemoryOrigin::MsmWriteCombined; check(m->fd >= 0, "allocate/export MSM surface", VA_STATUS_ERROR_ALLOCATION_FAILED); trace("allocated surface %ux%u fourcc=%#x pitch=%u size=%zu coherent=%d", width, height, fourcc, m->stride, m->size, coherent); return m; } void fill_neutral_surface(Memory &memory, unsigned width, unsigned height) { check(memory.fourcc == VA_FOURCC_NV12 || memory.fourcc == VA_FOURCC_P010, "unsupported replacement surface format", VA_STATUS_ERROR_INVALID_IMAGE_FORMAT); check(memory.origin != MemoryOrigin::Iris, "cannot overwrite decoder reference storage"); unsigned bytes = memory.fourcc == VA_FOURCC_P010 ? 2 : 1; validate(memory, width, height, bytes); auto *data = memory.map(); CpuAccess write(memory, DMA_BUF_SYNC_WRITE); // Midpoint luma and chroma produce neutral gray in both full and limited // range. P010 stores its ten significant bits in the high bits of a word. const uint16_t gray = 512u << 6; for (unsigned plane = 0; plane < 2; ++plane) { size_t offset = memory.data_offset + (plane ? size_t(memory.stride) * memory.storage_height : 0); unsigned rows = plane ? (height + 1) / 2 : height; unsigned samples = plane ? ((width + 1) & ~1u) : width; for (unsigned row = 0; row < rows; ++row) { auto *dst = data + offset + size_t(row) * memory.stride; if (bytes == 1) { std::memset(dst, 128, samples); } else { for (unsigned column = 0; column < samples; ++column) std::memcpy(dst + column * bytes, &gray, sizeof(gray)); } } } write.finish(); } void copy_surface(Memory &destination, Memory &source, unsigned width, unsigned height) { check(destination.fourcc == source.fourcc && (source.fourcc == VA_FOURCC_NV12 || source.fourcc == VA_FOURCC_P010), "persistent surface copy format mismatch", VA_STATUS_ERROR_INVALID_IMAGE_FORMAT); unsigned bytes = source.fourcc == VA_FOURCC_P010 ? 2 : 1; validate(source, width, height, bytes); validate(destination, width, height, bytes); auto start = std::chrono::steady_clock::now(); auto *src = source.map(); auto *dst = destination.map(); CpuAccess read(source, DMA_BUF_SYNC_READ); CpuAccess write(destination, DMA_BUF_SYNC_WRITE); for (unsigned plane = 0; plane < 2; ++plane) { size_t src_offset = source.data_offset + (plane ? size_t(source.stride) * source.storage_height : 0); size_t dst_offset = destination.data_offset + (plane ? size_t(destination.stride) * destination.storage_height : 0); unsigned rows = plane ? (height + 1) / 2 : height; unsigned row_bytes = (plane ? ((width + 1) & ~1u) : width) * bytes; for (unsigned row = 0; row < rows; ++row) std::memcpy(dst + dst_offset + size_t(row) * destination.stride, src + src_offset + size_t(row) * source.stride, row_bytes); } write.finish(); read.finish(); if (debug()) { auto us = std::chrono::duration_cast( std::chrono::steady_clock::now() - start) .count(); trace("surface copy mode=cpu %ux%u fourcc=%#x capture=%u time-us=%lld", width, height, source.fourcc, source.index, (long long)us); } } } // namespace irisva qcom-va-driver-1.1.0/src/vp9.cpp000066400000000000000000000102151526764642600163770ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "internal.hpp" namespace irisva { std::vector vp9_bitstream(VAProfile profile, const Vp9Picture &picture) { check(picture.has_params && picture.slices.size() == 1 && picture.pending_slices.empty(), "VP9 requires one complete frame per picture", VA_STATUS_ERROR_INVALID_BUFFER); const auto &p = picture.params; unsigned expected_profile = profile == VAProfileVP9Profile2 ? 2 : 0; check(p.profile == expected_profile && p.bit_depth == (expected_profile ? 10 : 8) && p.pic_fields.bits.subsampling_x && p.pic_fields.bits.subsampling_y, "VP9 supports Profile 0 8-bit / Profile 2 10-bit 4:2:0", VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT); const auto &bytes = picture.slices[0].bytes; // FFmpeg and GStreamer supply the original complete frame. Partial // partition buffers allowed by VA cannot drive a stateful decoder. check(bytes.size() >= 3 && p.frame_header_length_in_bytes && p.first_partition_size && size_t(p.frame_header_length_in_bytes) + p.first_partition_size <= bytes.size(), "VP9 full uncompressed and compressed headers required", VA_STATUS_ERROR_INVALID_BUFFER); size_t pos = 0; auto bit = [&]() { check(pos < bytes.size() * 8, "truncated VP9 header", VA_STATUS_ERROR_INVALID_BUFFER); size_t n = pos++; return (bytes[n / 8] >> (7 - n % 8)) & 1u; }; unsigned marker = bit() << 1; marker |= bit(); unsigned raw_profile = bit(); raw_profile |= bit() << 1; check(marker == 2 && raw_profile == expected_profile, "VP9 raw header/profile mismatch", VA_STATUS_ERROR_INVALID_BUFFER); check(!bit(), "VP9 show-existing must reuse the existing VA surface", VA_STATUS_ERROR_UNIMPLEMENTED); unsigned frame_type = bit(), show_frame = bit(), error_resilient = bit(); check(frame_type == p.pic_fields.bits.frame_type && show_frame == p.pic_fields.bits.show_frame && error_resilient == p.pic_fields.bits.error_resilient_mode, "VP9 raw header/VA flags mismatch", VA_STATUS_ERROR_INVALID_BUFFER); trace("VP9 frame profile=%u show=%u key=%u bytes=%zu", raw_profile, show_frame, !frame_type, bytes.size()); if (show_frame) return bytes; // Stateful decoders suppress invisible alt-ref output, whereas VA needs // a completed surface even for a reference-only picture. Keep the raw // frame and all entropy/MV state unchanged, then ask VP9 to display the // freshly refreshed reference slot. One superframe produces one output. auto bits = [&](unsigned count) { unsigned v = 0; while (count--) v = (v << 1) | bit(); return v; }; unsigned refresh = 0xff; if (frame_type) { unsigned intra_only = bit(); if (!error_resilient) bits(2); // reset_frame_context if (intra_only) { check(bits(24) == 0x498342, "VP9 intra sync code", VA_STATUS_ERROR_INVALID_BUFFER); if (raw_profile) { check(!bit(), "VP9 12-bit unsupported", VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT); check(bits(3) != 7, "VP9 RGB unsupported", VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT); bit(); // color_range; Profile 2 has implicit 4:2:0 } } refresh = bits(8); } check(refresh, "VP9 invisible frame without a refreshed reference slot", VA_STATUS_ERROR_UNIMPLEMENTED); unsigned slot = 0; while (!(refresh & (1u << slot))) ++slot; std::vector out = bytes; out.push_back(uint8_t(0x88 | (raw_profile << 3) | slot)); unsigned magnitude = 1; while (magnitude < 4 && bytes.size() >= (uint64_t(1) << (8 * magnitude))) ++magnitude; uint8_t marker_index = 0xc0 | ((magnitude - 1) << 3) | 1; out.push_back(marker_index); for (unsigned n : {unsigned(bytes.size()), 1u}) for (unsigned i = 0; i < magnitude; ++i) out.push_back(uint8_t(n >> (8 * i))); out.push_back(marker_index); trace("VP9 invisible reference slot=%u superframe bytes=%zu", slot, out.size()); return out; } } // namespace irisva qcom-va-driver-1.1.0/src/vpp.cpp000066400000000000000000000710351526764642600164750ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "internal.hpp" #include "overlay-params.h" #include #ifdef HAVE_CDSP_VPP #include "cdsp/client.h" #include #endif #ifdef HAVE_VULKAN_VPP #include "vulkan/vpp.hpp" #endif #if defined(HAVE_CDSP_VPP) || defined(HAVE_VULKAN_VPP) #include #include #include #include #include #include #include #endif namespace irisva { #ifdef HAVE_CDSP_VPP namespace { struct CdspApi { void *library = nullptr; decltype(&cdsp_vpp_create) create = nullptr; decltype(&cdsp_vpp_destroy) destroy = nullptr; decltype(&cdsp_vpp_import) import = nullptr; decltype(&cdsp_vpp_release) release = nullptr; decltype(&cdsp_vpp_transform) transform = nullptr; decltype(&cdsp_vpp_overlay) overlay = nullptr; ~CdspApi() { if (library) dlclose(library); } }; std::shared_ptr load_cdsp_api() { static std::mutex lock; static std::weak_ptr cached; std::lock_guard guard(lock); if (auto api = cached.lock()) return api; auto api = std::make_shared(); api->library = dlopen("libiris_vpp.so", RTLD_NOW | RTLD_LOCAL); if (!api->library) api->library = dlopen(IRIS_VPP_CLIENT_PATH, RTLD_NOW | RTLD_LOCAL); if (!api->library) { trace("CDSP VPP runtime unavailable: %s", dlerror()); return {}; } #define LOAD(name) \ api->name = reinterpret_castname)>(dlsym(api->library, "cdsp_vpp_" #name)); \ if (!api->name) \ return {}; LOAD(create) LOAD(destroy) LOAD(import) LOAD(release) LOAD(transform) LOAD(overlay) #undef LOAD cached = api; return api; } } // namespace #endif bool vpp_supported(VAProfile profile, VAEntrypoint entrypoint) { #ifdef HAVE_VULKAN_VPP return profile == VAProfileNone && entrypoint == VAEntrypointVideoProc; #elif defined(HAVE_CDSP_VPP) return profile == VAProfileNone && entrypoint == VAEntrypointVideoProc && bool(load_cdsp_api()); #else (void)profile; (void)entrypoint; return false; #endif } void vpp_caps(VAProcPipelineCaps &caps, bool tone_mapping) { // These are driver-owned arrays, as consumed by FFmpeg and other VA clients. // Do not read the incoming structure: clients may pass it uninitialized. static VAProcColorStandardType colors[] = {VAProcColorStandardNone, VAProcColorStandardBT601, VAProcColorStandardBT709, VAProcColorStandardBT2020, VAProcColorStandardExplicit}; static uint32_t formats[] = {VA_FOURCC_NV12, VA_FOURCC_P010, VA_FOURCC_BGRA, VA_FOURCC_RGBA}; caps = {}; caps.input_color_standards = caps.output_color_standards = colors; caps.num_input_color_standards = caps.num_output_color_standards = sizeof(colors) / sizeof(colors[0]); caps.input_pixel_format = caps.output_pixel_format = formats; caps.num_input_pixel_formats = 4; caps.num_output_pixel_formats = 2; caps.blend_flags = VA_BLEND_GLOBAL_ALPHA | VA_BLEND_PREMULTIPLIED_ALPHA; if (tone_mapping) { caps.input_color_standards = &colors[3]; caps.output_color_standards = &colors[2]; caps.num_input_color_standards = caps.num_output_color_standards = 1; caps.input_pixel_format = &formats[1]; caps.output_pixel_format = &formats[0]; caps.num_input_pixel_formats = caps.num_output_pixel_formats = 1; } caps.min_input_width = caps.min_output_width = 2; #if defined(HAVE_CDSP_VPP) || defined(HAVE_VULKAN_VPP) caps.rotation_flags = (1 << VA_ROTATION_NONE) | (1 << VA_ROTATION_90) | (1 << VA_ROTATION_180) | (1 << VA_ROTATION_270); #endif caps.min_input_height = caps.min_output_height = 2; caps.max_input_width = caps.max_output_width = 8192; caps.max_input_height = caps.max_output_height = 8192; } void vpp_filter(const Buffer &buffer) { check(buffer.type == VAProcFilterParameterBufferType && buffer.elements == 1 && buffer.element_size >= sizeof(VAProcFilterParameterBufferHDRToneMapping), "invalid VPP filter buffer", VA_STATUS_ERROR_INVALID_BUFFER); VAProcFilterParameterBufferHDRToneMapping filter{}; std::memcpy(&filter, buffer.data.data(), sizeof(filter)); check(filter.type == VAProcFilterHighDynamicRangeToneMapping, "unsupported VPP filter", VA_STATUS_ERROR_UNSUPPORTED_FILTER); } VppPicture vpp_picture(const Buffer &buffer, const std::shared_ptr &source, const Surface &target, const Buffer *filter, bool overlay) { VAProcPipelineParameterBuffer p{}; std::memcpy(&p, buffer.data.data(), sizeof(p)); check(!p.num_forward_references && !p.num_backward_references && !p.mirror_state && !p.num_additional_outputs && !(p.pipeline_flags & ~VA_PROC_PIPELINE_FAST) && !(p.filter_flags & ~VA_FILTER_SCALING_MASK) && !p.input_surface_flag && !p.output_surface_flag && !p.output_hdr_metadata, "unsupported VPP processing operation", VA_STATUS_ERROR_UNIMPLEMENTED); check(p.rotation_state <= VA_ROTATION_270, "invalid VPP rotation", VA_STATUS_ERROR_INVALID_PARAMETER); const auto &in = p.input_color_properties; const auto &out = p.output_color_properties; auto matches = [](unsigned a, unsigned b) { return !a || !b || a == b; }; if (!filter && !overlay) { check(matches(p.surface_color_standard, p.output_color_standard) && matches(in.color_range, out.color_range) && matches(in.chroma_sample_location, out.chroma_sample_location) && matches(in.colour_primaries, out.colour_primaries) && matches(in.transfer_characteristics, out.transfer_characteristics) && matches(in.matrix_coefficients, out.matrix_coefficients), "VPP color conversion unsupported", VA_STATUS_ERROR_UNIMPLEMENTED); } else if (filter) { auto color = [](unsigned value, unsigned expected) { return !value || value == 2 || value == expected; // ISO unspecified = 2. }; check((p.surface_color_standard == VAProcColorStandardNone || p.surface_color_standard == VAProcColorStandardBT2020 || p.surface_color_standard == VAProcColorStandardExplicit) && (p.output_color_standard == VAProcColorStandardNone || p.output_color_standard == VAProcColorStandardBT709 || p.output_color_standard == VAProcColorStandardExplicit) && color(in.colour_primaries, 9) && color(in.transfer_characteristics, 16) && color(in.matrix_coefficients, 9) && color(out.colour_primaries, 1) && color(out.transfer_characteristics, 1) && color(out.matrix_coefficients, 1) && matches(in.color_range, VA_SOURCE_RANGE_REDUCED) && matches(out.color_range, VA_SOURCE_RANGE_REDUCED) && matches(in.chroma_sample_location, out.chroma_sample_location), "tone mapping requires limited-range BT.2020/PQ to BT.709", VA_STATUS_ERROR_UNIMPLEMENTED); check(source->fourcc == VA_FOURCC_P010 && target.fourcc == VA_FOURCC_NV12, "tone mapping requires P010 input and NV12 output", VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT); } VppPicture picture; picture.color_standard = p.output_color_standard; picture.color_properties = p.output_color_properties; picture.output = p.output_region ? *p.output_region : VARectangle{0, 0, static_cast(target.width), static_cast(target.height)}; if (p.blend_state) picture.blend = *p.blend_state; if (overlay) { check(!filter && !p.rotation_state && is_rgb(source->fourcc) && source.get() != &target, "overlay requires a distinct BGRA/RGBA surface without filters or rotation", VA_STATUS_ERROR_UNIMPLEMENTED); check(!(picture.blend.flags & ~(VA_BLEND_GLOBAL_ALPHA | VA_BLEND_PREMULTIPLIED_ALPHA)) && (!(picture.blend.flags & VA_BLEND_GLOBAL_ALPHA) || (std::isfinite(picture.blend.global_alpha) && picture.blend.global_alpha >= 0 && picture.blend.global_alpha <= 1)), "unsupported overlay blend state", VA_STATUS_ERROR_INVALID_PARAMETER); picture.source = source; picture.region = p.surface_region ? *p.surface_region : VARectangle{0, 0, static_cast(source->width), static_cast(source->height)}; auto r = picture.region; check(r.x >= 0 && r.y >= 0 && r.width && r.height && unsigned(r.x) + r.width <= source->width && unsigned(r.y) + r.height <= source->height && picture.output.width && picture.output.height && picture.output.width <= 8192 && picture.output.height <= 8192, "invalid overlay rectangle", VA_STATUS_ERROR_INVALID_PARAMETER); return picture; } check(!p.blend_state && !picture.output.x && !picture.output.y && picture.output.width == target.width && picture.output.height == target.height, "VPP base layer must cover the output without blending", VA_STATUS_ERROR_UNIMPLEMENTED); if (filter) { vpp_filter(*filter); VAProcFilterParameterBufferHDRToneMapping hdr{}; std::memcpy(&hdr, filter->data.data(), sizeof(hdr)); check(hdr.data.metadata_type == VAProcHighDynamicRangeMetadataHDR10 && hdr.data.metadata && hdr.data.metadata_size == sizeof(VAHdrMetaDataHDR10), "invalid HDR10 input metadata", VA_STATUS_ERROR_INVALID_PARAMETER); // Snapshot caller-owned metadata at RenderPicture; never retain its pointer. VAHdrMetaDataHDR10 metadata{}; std::memcpy(&metadata, hdr.data.metadata, sizeof(metadata)); picture.tone_mapping = true; float peak = metadata.max_content_light_level; if (!peak) peak = metadata.max_display_mastering_luminance / 10000.0f; picture.source_peak = peak ? std::clamp(peak, 100.0f, 10000.0f) : 1000.0f; } picture.source = source; picture.rotation = p.rotation_state; picture.region = p.surface_region ? *p.surface_region : VARectangle{0, 0, static_cast(source->width), static_cast(source->height)}; auto r = picture.region; check(r.x >= 0 && r.y >= 0 && r.width && r.height && !(r.x & 1) && !(r.y & 1) && !(r.width & 1) && !(r.height & 1) && !(target.width & 1) && !(target.height & 1) && unsigned(r.x) + r.width <= source->width && unsigned(r.y) + r.height <= source->height, "VPP requires even dimensions and crop coordinates", VA_STATUS_ERROR_INVALID_PARAMETER); unsigned scaled_width = (picture.rotation & 1) ? target.height : target.width; unsigned scaled_height = (picture.rotation & 1) ? target.width : target.height; bool down = scaled_width <= r.width && scaled_height <= r.height; bool up = scaled_width >= r.width && scaled_height >= r.height; check((down || up) && uint64_t(scaled_width) * 20 >= r.width && uint64_t(scaled_height) * 20 >= r.height, "VPP cannot mix up/down scaling axes or downscale beyond 20:1", VA_STATUS_ERROR_RESOLUTION_NOT_SUPPORTED); check(source.get() != &target && (source->fourcc == VA_FOURCC_NV12 || source->fourcc == VA_FOURCC_P010) && (target.fourcc == VA_FOURCC_NV12 || target.fourcc == VA_FOURCC_P010), "VPP requires distinct NV12/P010 surfaces", VA_STATUS_ERROR_INVALID_SURFACE); return picture; } #if defined(HAVE_CDSP_VPP) || defined(HAVE_VULKAN_VPP) namespace { std::shared_ptr allocate_vpp_memory(int render_fd, const Surface &surface) { auto memory = std::make_shared(); memory->origin = MemoryOrigin::DmaHeap; memory->width = surface.width; memory->height = surface.height; memory->fourcc = surface.fourcc; unsigned bytes = surface.fourcc == VA_FOURCC_P010 ? 2 : 1; memory->stride = (surface.width * bytes + 127) & ~127u; memory->storage_height = (surface.height + 31) & ~31u; memory->size = (size_t(memory->stride) * memory->storage_height * 3 / 2 + 4095) & ~size_t(4095); int heap = open("/dev/dma_heap/system", O_RDONLY | O_CLOEXEC); const char *heap_operation = "open system DMA heap"; int heap_error = errno; if (heap >= 0) { dma_heap_allocation_data data{}; data.len = memory->size; data.fd_flags = O_RDWR | O_CLOEXEC; int result = ioctl(heap, DMA_HEAP_IOCTL_ALLOC, &data); heap_error = errno; close(heap); if (result == 0) { memory->fd = data.fd; return memory; } heap_operation = "allocate from system DMA heap"; } trace("%s failed (errno=%d); trying coherent MSM GEM", heap_operation, heap_error); // Buffers can cross Vulkan/CDSP backends. WC GEM produces incorrect pixels // on the tested CDSP stack, so do not use the ordinary allocator's WC fallback. memory->fd = irisva_msm_allocate_coherent(render_fd, memory->size); if (memory->fd < 0) { int gem_error = errno; throw Error(VA_STATUS_ERROR_ALLOCATION_FAILED, std::string(heap_operation) + " failed (errno=" + std::to_string(heap_error) + "); allocate/export coherent MSM GEM failed (errno=" + std::to_string(gem_error) + ")"); } memory->origin = MemoryOrigin::MsmCoherent; return memory; } #ifdef HAVE_CDSP_VPP cdsp_vpp_frame import_vpp_frame(Memory &memory, const Surface &surface, const std::shared_ptr &api) { unsigned row_bytes = surface.width * (is_rgb(surface.fourcc) ? 4 : surface.fourcc == VA_FOURCC_P010 ? 2 : 1); check(memory.fd >= 0 && memory.fourcc == surface.fourcc && memory.size <= INT_MAX && memory.width >= surface.width && memory.height >= surface.height && memory.stride >= row_bytes && memory.storage_height >= surface.height && uint64_t(memory.data_offset) + uint64_t(memory.stride) * (is_rgb(surface.fourcc) ? surface.height - 1 : uint64_t(memory.storage_height) + surface.height / 2 - 1) + row_bytes <= memory.size, "unsupported CDSP DMA-BUF layout", VA_STATUS_ERROR_INVALID_SURFACE); if (!memory.vpp_registration) { cdsp_vpp_buffer *buffer = nullptr; int e = api->import(memory.fd, memory.size, &buffer); check(e == 0, "CDSP DMA-BUF import failed"); // shared_ptr invokes this deleter even if allocating its control block fails. // Keep the runtime loaded until every retained map has been released. memory.vpp_registration = std::shared_ptr( buffer, [api](void *p) { api->release(static_cast(p)); }); } return {static_cast(memory.vpp_registration.get()), surface.width, surface.height, memory.stride, memory.data_offset, memory.data_offset + size_t(memory.stride) * memory.storage_height}; } #endif } // namespace struct Vpp::Impl { int render_fd; std::shared_ptr intermediate; #ifdef HAVE_VULKAN_VPP bool vulkan_probed = false; std::unique_ptr vulkan; #endif #ifdef HAVE_CDSP_VPP std::shared_ptr api; cdsp_vpp_context *context = nullptr; bool cdsp_unavailable = false; void initialize_cdsp() { if (context) return; api = load_cdsp_api(); check(bool(api), "CDSP VPP runtime unavailable", VA_STATUS_ERROR_UNSUPPORTED_ENTRYPOINT); int e = api->create(&context, CDSP_VPP_MAX_WORKERS, 1); // Capped by available HVX contexts. if (e) { trace("CDSP VPP initialization failed: %d", e); throw Error(VA_STATUS_ERROR_OPERATION_FAILED, "CDSP VPP initialization failed"); } cdsp_unavailable = false; } ~Impl() { if (context) api->destroy(context); } #endif explicit Impl(int fd) : render_fd(fd) {} }; Vpp::Vpp(int render_fd) : impl_(std::make_unique(render_fd)) {} bool Vpp::vulkan_available() { #ifdef HAVE_VULKAN_VPP if (!impl_->vulkan_probed) { impl_->vulkan_probed = true; try { impl_->vulkan = std::make_unique(impl_->render_fd); } catch (const Error &error) { trace("Vulkan VPP unavailable: %s", error.what()); } } return bool(impl_->vulkan); #else return false; #endif } bool Vpp::use_vulkan(bool overlay) { const char *requested = overlay ? std::getenv("IRIS_VAAPI_OVERLAY") : nullptr; if (!requested) requested = std::getenv("IRIS_VAAPI_VPP"); if (!requested) requested = IRIS_VPP_DEFAULT_BACKEND; check(!std::strcmp(requested, "auto") || !std::strcmp(requested, "cdsp") || !std::strcmp(requested, "vulkan"), "VPP backend must be auto, cdsp or vulkan", VA_STATUS_ERROR_INVALID_PARAMETER); bool automatic = !std::strcmp(requested, "auto"); if (!std::strcmp(requested, "vulkan") || (automatic && overlay)) { if (vulkan_available()) return true; check(automatic, "Vulkan VPP unavailable", VA_STATUS_ERROR_UNSUPPORTED_ENTRYPOINT); } #ifdef HAVE_CDSP_VPP if (automatic) { if (!impl_->cdsp_unavailable) { try { impl_->initialize_cdsp(); return false; } catch (const Error &error) { impl_->cdsp_unavailable = true; trace("CDSP VPP unavailable: %s", error.what()); } } } else { impl_->initialize_cdsp(); return false; } #else check(automatic, "CDSP VPP was disabled at build time", VA_STATUS_ERROR_UNSUPPORTED_ENTRYPOINT); #endif check(vulkan_available(), "No VPP backend available", VA_STATUS_ERROR_UNSUPPORTED_ENTRYPOINT); return true; } bool Vpp::tone_mapping_supported() { return vulkan_available(); } void Vpp::process_single(const VppPicture &picture, const std::shared_ptr &target) { auto source = picture.source; auto region = picture.region; check(source && source->memory, "VPP source has no storage", VA_STATUS_ERROR_INVALID_SURFACE); if (!target->memory) target->memory = allocate_vpp_memory(impl_->render_fd, *target); auto &src = *source->memory; auto &dst = *target->memory; check(dst.origin != MemoryOrigin::Iris, "VPP cannot overwrite decoder reference storage", VA_STATUS_ERROR_INVALID_SURFACE); struct stat src_stat{}, dst_stat{}; check(fstat(src.fd, &src_stat) == 0 && fstat(dst.fd, &dst_stat) == 0 && (src_stat.st_dev != dst_stat.st_dev || src_stat.st_ino != dst_stat.st_ino), "VPP source and destination DMA-BUF alias", VA_STATUS_ERROR_INVALID_SURFACE); #ifdef HAVE_VULKAN_VPP if (picture.tone_mapping) { check(tone_mapping_supported(), "Vulkan tone mapping unavailable", VA_STATUS_ERROR_UNSUPPORTED_FILTER); bool geometry = picture.rotation || region.width != target->width || region.height != target->height; if (!geometry) { impl_->vulkan->process(picture, *target); target->status = VA_STATUS_SUCCESS; return; } auto &intermediate = impl_->intermediate; if (!intermediate || intermediate->width != region.width || intermediate->height != region.height) { auto resized = std::make_shared(); resized->width = region.width; resized->height = region.height; resized->fourcc = VA_FOURCC_NV12; resized->memory = allocate_vpp_memory(impl_->render_fd, *resized); intermediate = std::move(resized); } // Vulkan and CDSP share this allocation without CPU pixel access. impl_->vulkan->process(picture, *intermediate); source = intermediate; region.x = region.y = 0; } #endif if (use_vulkan(false)) { #ifdef HAVE_VULKAN_VPP auto transform = picture; transform.source = source; transform.region = region; impl_->vulkan->transform(transform, *target); target->status = VA_STATUS_SUCCESS; return; #endif } #ifdef HAVE_CDSP_VPP impl_->initialize_cdsp(); auto in = import_vpp_frame(*source->memory, *source, impl_->api), out = import_vpp_frame(dst, *target, impl_->api); cdsp_vpp_crop crop{unsigned(region.x), unsigned(region.y), region.width, region.height}; uint64_t dsp_us = 0; int e = impl_->api->transform( impl_->context, &in, source->fourcc == VA_FOURCC_P010 ? CDSP_VPP_P010 : CDSP_VPP_NV12, &crop, &out, target->fourcc == VA_FOURCC_P010 ? CDSP_VPP_P010 : CDSP_VPP_NV12, picture.rotation, &dsp_us); if (e) { trace("CDSP VPP processing failed: %d", e); throw Error(VA_STATUS_ERROR_OPERATION_FAILED, "CDSP VPP processing failed"); } target->status = VA_STATUS_SUCCESS; trace("VPP CDSP %ux%u -> %ux%u rotation=%u src-fd=%d dst-fd=%d DSP=%llu us", region.width, region.height, target->width, target->height, picture.rotation * 90, src.fd, dst.fd, static_cast(dsp_us)); #else throw Error(VA_STATUS_ERROR_UNIMPLEMENTED, "processing requires CDSP VPP"); #endif } namespace { OverlayParams overlay_parameters(const VppPicture &layer, const Surface &base, const Surface &target, const VppPicture &background) { const auto &src = *base.memory, &dst = *target.memory, &rgb = *layer.source->memory; auto r = layer.region, out = layer.output; OverlayParams p{}; p.width = target.width; p.height = target.height; p.src_stride = src.stride; p.src_y = src.data_offset; p.src_uv = src.data_offset + src.stride * src.storage_height; p.dst_stride = dst.stride; p.dst_y = dst.data_offset; p.dst_uv = dst.data_offset + dst.stride * dst.storage_height; p.rgb_stride = rgb.stride; p.rgb_offset = rgb.data_offset + r.y * rgb.stride + r.x * 4; p.rgb_width = r.width; p.rgb_height = r.height; p.x = out.x; p.y = out.y; p.w = out.width; p.h = out.height; p.alpha = (layer.blend.flags & VA_BLEND_GLOBAL_ALPHA) ? std::lround(layer.blend.global_alpha * 256) : 256; p.flags = (target.fourcc == VA_FOURCC_P010 ? OVERLAY_P010 : 0) | (layer.source->fourcc == VA_FOURCC_BGRA ? OVERLAY_BGRA : 0) | (layer.blend.flags & VA_BLEND_PREMULTIPLIED_ALPHA ? OVERLAY_PREMULTIPLIED : 0); p.step_x = (p.rgb_width << 16) / p.w; p.step_y = (p.rgb_height << 16) / p.h; auto standard = background.color_standard; auto props = background.color_properties; double kr = 0.2126, kb = 0.0722; if (standard == VAProcColorStandardBT601 || ((standard == VAProcColorStandardExplicit || standard == VAProcColorStandardNone) && (props.matrix_coefficients == 5 || props.matrix_coefficients == 6))) { kr = 0.299; kb = 0.114; } else if (standard == VAProcColorStandardBT2020 || ((standard == VAProcColorStandardExplicit || standard == VAProcColorStandardNone) && props.matrix_coefficients == 9)) { kr = 0.2627; kb = 0.0593; } else { check((standard == VAProcColorStandardNone || standard == VAProcColorStandardBT709 || standard == VAProcColorStandardExplicit) && (!props.matrix_coefficients || props.matrix_coefficients == 1 || props.matrix_coefficients == 2), "unsupported overlay output matrix", VA_STATUS_ERROR_UNIMPLEMENTED); } check(!props.color_range || props.color_range == VA_SOURCE_RANGE_REDUCED || props.color_range == VA_SOURCE_RANGE_FULL, "unsupported overlay output range", VA_STATUS_ERROR_UNIMPLEMENTED); double kg = 1 - kr - kb; bool full = props.color_range == VA_SOURCE_RANGE_FULL; double ys = full ? 1 : 219.0 / 255, cs = full ? 1 : 224.0 / 255; double matrix[] = {kr * ys, kg * ys, kb * ys, -kr * cs / (2 * (1 - kb)), -kg * cs / (2 * (1 - kb)), cs / 2, cs / 2, -kg * cs / (2 * (1 - kr)), -kb * cs / (2 * (1 - kr))}; for (unsigned i = 0; i < 9; ++i) p.matrix[i] = std::lround(matrix[i] * 16384); p.black = full ? 0 : 16; check(overlay_valid(&p, src.size, rgb.size, dst.size), "invalid overlay memory layout", VA_STATUS_ERROR_INVALID_SURFACE); return p; } } // namespace void Vpp::process(const std::vector &pictures, const std::shared_ptr &target) { check(!pictures.empty(), "missing VPP base layer", VA_STATUS_ERROR_INVALID_PARAMETER); if (!target->memory) target->memory = allocate_vpp_memory(impl_->render_fd, *target); check(target->memory->origin != MemoryOrigin::Iris, "VPP cannot overwrite decoder references", VA_STATUS_ERROR_INVALID_SURFACE); struct stat dst_stat{}; check(fstat(target->memory->fd, &dst_stat) == 0, "stat VPP output"); for (const auto &picture : pictures) { check(picture.source && picture.source->memory, "VPP source has no storage", VA_STATUS_ERROR_INVALID_SURFACE); struct stat src_stat{}; check(fstat(picture.source->memory->fd, &src_stat) == 0 && (src_stat.st_dev != dst_stat.st_dev || src_stat.st_ino != dst_stat.st_ino), "VPP source and destination DMA-BUF alias", VA_STATUS_ERROR_INVALID_SURFACE); } auto base = pictures[0].source; const auto &background = pictures[0]; bool direct = !background.tone_mapping && !background.rotation && !background.region.x && !background.region.y && background.region.width == target->width && background.region.height == target->height && base->fourcc == target->fourcc; std::vector params; for (size_t i = 1; i < pictures.size(); ++i) params.push_back(overlay_parameters(pictures[i], direct && i == 1 ? *base : *target, *target, background)); if (pictures.size() == 1) { process_single(background, target); return; } bool gpu = use_vulkan(true); if (!direct) { process_single(background, target); base = target; } for (size_t i = 1; i < pictures.size(); ++i) { #ifdef HAVE_VULKAN_VPP if (gpu) impl_->vulkan->overlay(*base, *pictures[i].source, *target, params[i - 1]); #endif #ifdef HAVE_CDSP_VPP if (!gpu) { auto src = import_vpp_frame(*base->memory, *base, impl_->api); auto rgb = import_vpp_frame(*pictures[i].source->memory, *pictures[i].source, impl_->api); auto dst = import_vpp_frame(*target->memory, *target, impl_->api); uint64_t us = 0; int error = impl_->api->overlay(impl_->context, src.buffer, rgb.buffer, dst.buffer, ¶ms[i - 1], &us); if (error) trace("CDSP overlay failed: %d", error); check(!error, "CDSP overlay failed"); trace("VPP CDSP overlay %dx%d -> %ux%u DSP=%llu us", params[i - 1].w, params[i - 1].h, target->width, target->height, (unsigned long long)us); } #endif base = target; } target->status = VA_STATUS_SUCCESS; } #else struct Vpp::Impl {}; Vpp::Vpp(int) { throw Error(VA_STATUS_ERROR_UNSUPPORTED_ENTRYPOINT, "VPP was disabled at build time"); } bool Vpp::tone_mapping_supported() { return false; } void Vpp::process(const std::vector &, const std::shared_ptr &) { throw Error(VA_STATUS_ERROR_UNSUPPORTED_ENTRYPOINT, "VPP was disabled at build time"); } #endif Vpp::~Vpp() = default; } // namespace irisva qcom-va-driver-1.1.0/src/vulkan/000077500000000000000000000000001526764642600164565ustar00rootroot00000000000000qcom-va-driver-1.1.0/src/vulkan/meson.build000066400000000000000000000020201526764642600206120ustar00rootroot00000000000000tonemap_spirv = custom_target( 'tonemap_spirv', input: 'tonemap.comp', output: 'tonemap_spirv.h', command: [glslang, '-V', '--target-env', 'vulkan1.1', '--vn', 'tonemap_spirv', '-o', '@OUTPUT@', '@INPUT@'], ) overlay_spirv = custom_target( 'overlay_spirv', input: 'overlay.comp', output: 'overlay_spirv.h', command: [glslang, '-V', '--target-env', 'vulkan1.1', '--vn', 'overlay_spirv', '-o', '@OUTPUT@', '@INPUT@'], ) transform_spirv = custom_target( 'transform_spirv', input: 'transform.comp', output: 'transform_spirv.h', command: [glslang, '-V', '--target-env', 'vulkan1.1', '--vn', 'transform_spirv', '-o', '@OUTPUT@', '@INPUT@'], ) vertical_spirv = custom_target( 'vertical_spirv', input: 'transform.comp', output: 'vertical_spirv.h', command: [glslang, '-V', '--target-env', 'vulkan1.1', '-DVERTICAL_PASS=1', '--vn', 'vertical_spirv', '-o', '@OUTPUT@', '@INPUT@'], ) vulkan_sources = [files('vpp.cpp'), tonemap_spirv, overlay_spirv, transform_spirv, vertical_spirv] qcom-va-driver-1.1.0/src/vulkan/overlay.comp000066400000000000000000000057451526764642600210320ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #version 450 #extension GL_EXT_shader_16bit_storage : require layout(local_size_x=64) in; layout(set=0,binding=0,std430) readonly buffer Base { uint16_t base[]; }; layout(set=0,binding=1,std430) buffer Output { uint16_t output_data[]; }; layout(set=0,binding=2,std430) readonly buffer RGB { uint rgba[]; }; // Matches OverlayParams in ../overlay-params.h. layout(push_constant,std430) uniform Parameters { int width,height,src_stride,src_y,src_uv,dst_stride,dst_y,dst_uv; int rgb_stride,rgb_offset,rgb_width,rgb_height,x,y,w,h,alpha,flags,step_x,step_y; int matrix[9]; int black; } p; ivec4 pixel(ivec2 xy) { uint bits=rgba[(p.rgb_offset+xy.y*p.rgb_stride+xy.x*4)/4]; ivec4 v=ivec4(bits&255u,(bits>>8)&255u,(bits>>16)&255u,bits>>24); if ((p.flags&2)!=0) v.rgb=v.bgr; v.rgb *= (p.flags&4)!=0 ? 255 : v.a; return v; } ivec4 interpolate(ivec4 a,ivec4 b,int w) { return (a*(256-w)+b*w+128)>>8; } ivec4 sample_rgb(ivec2 xy) { xy-=ivec2(p.x,p.y); if (p.alpha==0 || any(lessThan(xy,ivec2(0))) || any(greaterThanEqual(xy,ivec2(p.w,p.h)))) return ivec4(0); ivec4 v; if (p.w==p.rgb_width && p.h==p.rgb_height) v=pixel(xy); else { ivec2 pos=clamp(xy*ivec2(p.step_x,p.step_y)+ivec2(p.step_x,p.step_y)/2-32768, ivec2(0),(ivec2(p.rgb_width,p.rgb_height)-1)<<16); ivec2 a=pos>>16,b=min(a+1,ivec2(p.rgb_width,p.rgb_height)-1),f=(pos>>8)&255; v=interpolate(interpolate(pixel(a),pixel(ivec2(b.x,a.y)),f.x), interpolate(pixel(ivec2(a.x,b.y)),pixel(b),f.x),f.y); } return (v*p.alpha+128)>>8; } int contribution(int row,ivec4 v) { return (p.matrix[row]*v.r+p.matrix[row+1]*v.g+p.matrix[row+2]*v.b+8192)>>14; } ivec2 load_pair(int offset,bool wide) { uint first=uint(base[offset/2]); return wide ? ivec2(first>>6,uint(base[offset/2+1])>>6) : ivec2(first&255u,first>>8); } void store_pair(int offset,ivec2 v,bool wide) { v=clamp(v,ivec2(0),ivec2(wide?1023:255)); if (wide) { output_data[offset/2]=uint16_t(v.x<<6); output_data[offset/2+1]=uint16_t(v.y<<6); } else output_data[offset/2]=uint16_t(v.x|(v.y<<8)); } void main() { int x=int(gl_GlobalInvocationID.x)*2,y=int(gl_GlobalInvocationID.y)*2; if (x>=p.width) return; bool wide=(p.flags&1)!=0; int bytes=wide?2:1,scale=wide?4:1; int sum_a=0,sum_u=0,sum_v=0; for (int row=0;row<2;++row) { ivec2 v=load_pair(p.src_y+(y+row)*p.src_stride+x*bytes,wide); for (int col=0;col<2;++col) { ivec4 rgb=sample_rgb(ivec2(x+col,y+row)); v[col]=(v[col]*(255-rgb.a)+p.black*scale*rgb.a+contribution(0,rgb)*scale+127)/255; sum_a+=rgb.a; sum_u+=contribution(3,rgb); sum_v+=contribution(6,rgb); } store_pair(p.dst_y+(y+row)*p.dst_stride+x*bytes,v,wide); } ivec2 uv=load_pair(p.src_uv+(y/2)*p.src_stride+x*bytes,wide); uv=(uv*(1020-sum_a)+128*scale*sum_a+ivec2(sum_u,sum_v)*scale+510)/1020; store_pair(p.dst_uv+(y/2)*p.dst_stride+x*bytes,uv,wide); } qcom-va-driver-1.1.0/src/vulkan/tonemap.comp000066400000000000000000000072231526764642600210050ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #version 450 #extension GL_EXT_shader_16bit_storage : require layout(local_size_x = 64) in; layout(set = 0, binding = 0, std430) readonly buffer Input { uint16_t src[]; }; layout(set = 0, binding = 1, std430) writeonly buffer Output { uint16_t dst[]; }; layout(push_constant) uniform Params { uint width, height, src_stride, src_y, src_uv, dst_stride, dst_y, dst_uv; float source_pq, destination, knee; } p; vec3 decode_pq(vec3 code) { vec3 t = pow(clamp(code, 0.0, 1.0), vec3(1.0 / 78.84375)); return 10000.0 * pow(max(t - 0.8359375, 0.0) / (18.8515625 - 18.6875*t), vec3(1.0 / 0.1593017578125)); } float encode_pq(float nits) { float t = pow(max(nits, 0.0) / 10000.0, 0.1593017578125); return pow((0.8359375 + 18.8515625*t) / (1.0 + 18.6875*t), 78.84375); } ivec3 tone(ivec3 yuv) { // Limited-range BT.2020 NCL / PQ to linear RGB, in cd/m². vec3 c = (vec3(yuv) - vec3(64, 512, 512)) / vec3(876, 896, 896); float r = c.x + 1.4746*c.z, b = c.x + 1.8814*c.y; vec3 rgb = decode_pq(vec3(r, (c.x - .2627*r - .0593*b)/.678, b)); rgb = vec3(dot(rgb, vec3(1.660491, -.587641, -.072850)), dot(rgb, vec3(-.124550, 1.132900, -.008349)), dot(rgb, vec3(-.018151, -.100579, 1.118730))); // Desaturate out-of-gamut negative channels at constant BT.709 luminance. float luma = max(dot(rgb, vec3(.2126, .7152, .0722)), 0.0); float negative = max(-min(min(rgb.x, rgb.y), rgb.z), 0.0); rgb = (rgb + negative) * (luma / max(luma + negative, 1e-20)); // BT.2390-style Hermite shoulder in PQ space, with a zero-black, // 100-nit SDR target. One gain for all channels preserves hue. float maximum = max(max(rgb.x, rgb.y), rgb.z); float mapped = maximum; if (p.destination < 1.0) { float x = min(encode_pq(maximum) / p.source_pq, 1.0); float t = clamp((x - p.knee) / (1.0 - p.knee), 0.0, 1.0); float shoulder = p.knee + (1.0 - p.knee)*t + (3.0*p.destination - p.knee - 2.0)*t*t + (p.knee + 1.0 - 2.0*p.destination)*t*t*t; mapped = decode_pq(vec3((x < p.knee ? x : shoulder)*p.source_pq)).x; } float gain = mapped / max(maximum, 1e-20) / 100.0; rgb = pow(clamp(rgb*gain, 0.0, 1.0), vec3(1.0/2.4)); float y = dot(rgb, vec3(.2126, .7152, .0722)); return ivec3(roundEven(vec3(64.0 + 876.0*y, 512.0 + (rgb.b-y)*(896.0/1.8556), 512.0 + (rgb.r-y)*(896.0/1.5748)))); } uint pack_pair(ivec2 value) { uvec2 bytes = uvec2(clamp(value, 0, 255)); return bytes.x | (bytes.y << 8); } void main() { // Each invocation owns 4x2 pixels. Halfword stores also handle widths // congruent to 2 modulo 4, without overlapping another invocation's bytes. uint x = gl_GlobalInvocationID.x*4u, y = gl_GlobalInvocationID.y*2u; if (x >= p.width || y >= p.height) return; for (uint pair = 0u; pair < 2u; ++pair) { uint column = x + pair*2u; if (column >= p.width) break; uint uv = (p.src_uv + (y/2u)*p.src_stride)/2u + column; ivec2 chroma = ivec2(uint(src[uv]) >> 6, uint(src[uv+1u]) >> 6); ivec2 sum = ivec2(0); for (uint row = 0u; row < 2u; ++row) { uint yy = (p.src_y + (y+row)*p.src_stride)/2u + column; ivec3 a = tone(ivec3(int(uint(src[yy]) >> 6), chroma)); ivec3 b = tone(ivec3(int(uint(src[yy+1u]) >> 6), chroma)); dst[(p.dst_y + (y+row)*p.dst_stride + column)/2u] = uint16_t(pack_pair((ivec2(a.x, b.x) + 2)/4)); sum += a.yz + b.yz; } dst[(p.dst_uv + (y/2u)*p.dst_stride + column)/2u] = uint16_t(pack_pair((sum+8)/16)); } } qcom-va-driver-1.1.0/src/vulkan/transform.comp000066400000000000000000000131501526764642600213510ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #version 450 #extension GL_EXT_shader_16bit_storage : require // Each invocation owns a complete 2x2 Y block and its UV pair. In particular, // neighboring invocations never write different bytes of the same NV12 word. // Specialize uniform format/rotation and tap counts so the driver can unroll // common 2x area and four-tap cubic kernels without per-sample branches. layout(constant_id=0) const int source_wide = 0; layout(constant_id=1) const int target_wide = 0; layout(constant_id=2) const int rotation = 0; layout(constant_id=3) const int filter_mode = 0; // copy, area, separable cubic/area layout(constant_id=4) const int taps_x = 0; layout(constant_id=5) const int taps_y = 0; layout(local_size_x=8, local_size_y=8) in; layout(set=0, binding=0, std430) readonly buffer Input { uint16_t input_data[]; }; layout(set=0, binding=1, std430) writeonly buffer Output { uint16_t output_data[]; }; layout(set=0, binding=3, std430) readonly buffer Filters { uint filters[]; }; layout(set=0, binding=4, std430) buffer Scratch { int16_t scratch[]; }; layout(push_constant, std430) uniform Parameters { int src_width, src_height, scaled_width, scaled_height, width, height; int src_stride, src_y, src_uv, dst_stride, dst_y, dst_uv; int src_wide, dst_wide, rotation, enlarging; int filter_x, filter_y, filter_uv_x, filter_uv_y; } p; ivec2 source_position(ivec2 output_pos, bool chroma) { ivec2 size = ivec2(p.scaled_width, p.scaled_height) / (chroma ? 2 : 1); if (rotation == 1) return ivec2(output_pos.y, size.y - 1 - output_pos.x); if (rotation == 2) return size - 1 - output_pos; if (rotation == 3) return ivec2(size.x - 1 - output_pos.y, output_pos.x); return output_pos; } int load_sample(ivec2 xy, bool chroma, int channel) { int x = chroma ? xy.x * 2 + channel : xy.x; int offset = (chroma ? p.src_uv : p.src_y) + xy.y * p.src_stride + x * (1 + source_wide); uint word = uint(input_data[offset / 2]); if (source_wide != 0) return int(word >> 6); return int((word >> ((offset & 1) * 8)) & 255u) << (target_wide != 0 ? 2 : 0); } int scratch_index(ivec2 xy, bool chroma, int channel) { return (chroma ? p.src_width * p.scaled_height : 0) + xy.y * p.src_width + (chroma ? xy.x * 2 + channel : xy.x); } // Vertical results retain Q6 precision for 8-bit cubic scaling and Q4 for // 10-bit processing. Their signed range fits int16, including cubic overshoot. int vertical(ivec2 xy, bool chroma, int channel) { int fy = (chroma ? p.filter_uv_y : p.filter_y) + xy.y * 24; int count = taps_y != 0 ? taps_y : int(filters[fy]); int shift = int(filters[fy + 1]); bool wide = (source_wide | target_wide) != 0; int fraction = wide ? 4 : filter_mode == 2 ? 6 : 0; int sum = 0; for (int j = 0; j < count; ++j) { uint tap = filters[fy + 2 + j]; sum += load_sample(ivec2(xy.x, tap & 65535u), chroma, channel) * (int(tap) >> 16); } int value = (sum + (1 << (shift - fraction - 1))) >> (shift - fraction); return !wide && filter_mode != 2 ? clamp(value, 0, 255) : value; } int filtered(ivec2 xy, bool chroma, int channel) { xy = source_position(xy, chroma); bool wide = (source_wide | target_wide) != 0; int result; if (filter_mode == 0) { result = load_sample(xy, chroma, channel); } else { // The CPU caches CDSP's quantized filters for each geometry. A tap is // packed as an unsigned 16-bit index and a signed 16-bit weight. int fx = (chroma ? p.filter_uv_x : p.filter_x) + xy.x * 24; int nx = taps_x != 0 ? taps_x : int(filters[fx]); int sx = int(filters[fx + 1]); int fraction = wide ? 4 : filter_mode == 2 ? 6 : 0; int sum = 0; for (int i = 0; i < nx; ++i) { uint tap = filters[fx + 2 + i]; ivec2 pos = ivec2(tap & 65535u, xy.y); int value = filter_mode >= 2 ? int(scratch[scratch_index(pos, chroma, channel)]) : vertical(pos, chroma, channel); sum += value * (int(tap) >> 16); } int shift = sx + fraction + (wide && target_wide == 0 ? 2 : 0); return clamp((sum + (1 << (shift - 1))) >> shift, 0, target_wide != 0 ? 1023 : 255); } if (wide && target_wide == 0) result = (result + 2) >> 2; return clamp(result, 0, target_wide != 0 ? 1023 : 255); } void store_pair(int offset, ivec2 value) { if (target_wide != 0) { output_data[offset / 2] = uint16_t(value.x << 6); output_data[offset / 2 + 1] = uint16_t(value.y << 6); } else { output_data[offset / 2] = uint16_t(value.x | (value.y << 8)); } } #ifdef VERTICAL_PASS void main() { ivec2 xy = ivec2(gl_GlobalInvocationID.xy) * 2; if (xy.x >= p.src_width || xy.y >= p.scaled_height) return; for (int row = 0; row < 2; ++row) for (int col = 0; col < 2; ++col) { ivec2 pos = xy + ivec2(col, row); scratch[scratch_index(pos, false, 0)] = int16_t(vertical(pos, false, 0)); } for (int ch = 0; ch < 2; ++ch) scratch[scratch_index(xy / 2, true, ch)] = int16_t(vertical(xy / 2, true, ch)); } #else void main() { ivec2 xy = ivec2(gl_GlobalInvocationID.xy) * 2; if (xy.x >= p.width || xy.y >= p.height) return; for (int row = 0; row < 2; ++row) { ivec2 a = xy + ivec2(0, row); store_pair(p.dst_y + a.y * p.dst_stride + a.x * (1 + target_wide), ivec2(filtered(a, false, 0), filtered(a + ivec2(1, 0), false, 0))); } store_pair(p.dst_uv + (xy.y / 2) * p.dst_stride + xy.x * (1 + target_wide), ivec2(filtered(xy / 2, true, 0), filtered(xy / 2, true, 1))); } #endif qcom-va-driver-1.1.0/src/vulkan/vpp.cpp000066400000000000000000001067131526764642600177770ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "../internal.hpp" #include "vpp.hpp" #include #include "tonemap_spirv.h" #include "overlay_spirv.h" #include "transform_spirv.h" #include "vertical_spirv.h" #include "../cdsp/filter.h" #include #include #include #include #include #include #include #include #include #include namespace irisva { namespace { void vk_check(VkResult result, const char *operation) { if (result != VK_SUCCESS) { trace("Vulkan %s failed: %d", operation, result); throw Error(VA_STATUS_ERROR_OPERATION_FAILED, operation); } } static_assert(sizeof(OverlayParams) == 120, "overlay push constant ABI"); struct Parameters { uint32_t width, height, src_stride, src_y, src_uv, dst_stride, dst_y, dst_uv; float source_pq, destination, knee; }; struct TransformParameters { int32_t src_width, src_height, scaled_width, scaled_height, width, height; int32_t src_stride, src_y, src_uv, dst_stride, dst_y, dst_uv; int32_t src_wide, dst_wide, rotation, enlarging; int32_t filter_x, filter_y, filter_uv_x, filter_uv_y; }; static_assert(sizeof(TransformParameters) <= sizeof(OverlayParams), "transform push constant ABI"); static_assert(sizeof(TransformParameters) == 80 && FILTER_MAX_TAPS == 22, "transform shader parameter and filter layout"); double encode_pq(double nits) { double power = std::pow(nits / 10000.0, 0.1593017578125); return std::pow((0.8359375 + 18.8515625 * power) / (1.0 + 18.6875 * power), 78.84375); } void validate_layout(const Memory &memory, const Surface &surface) { unsigned row_bytes = surface.width * (surface.fourcc == VA_FOURCC_P010 ? 2 : 1); check(memory.fd >= 0 && memory.fourcc == surface.fourcc && memory.width >= surface.width && memory.height >= surface.height && memory.stride >= row_bytes && memory.storage_height >= surface.height && !(memory.stride & 1) && !(memory.data_offset & 1) && memory.size <= UINT32_MAX && uint64_t(memory.data_offset) + uint64_t(memory.stride) * (uint64_t(memory.storage_height) + surface.height / 2 - 1) + row_bytes <= memory.size, "unsupported Vulkan DMA-BUF layout", VA_STATUS_ERROR_INVALID_SURFACE); } void publish_dma_buf(const Memory &memory) { // Extra exporter synchronization fixes GPU-to-CDSP visibility on the // tested SC8280XP stack. These CPU-access hooks neither map nor copy // pixels; this is a workaround, not a generic device-cache flush. for (uint64_t phase : {DMA_BUF_SYNC_START, DMA_BUF_SYNC_END}) { dma_buf_sync sync{phase | DMA_BUF_SYNC_RW}; int result; do { result = ioctl(memory.fd, DMA_BUF_IOCTL_SYNC, &sync); } while (result < 0 && errno == EINTR); check(result == 0, "publish Vulkan DMA-BUF writes"); } } } // namespace struct VulkanVpp::Impl : std::enable_shared_from_this { // Imported buffers retain the device, even after the VPP context is destroyed. // Memory releases its registration before closing the original DMA-BUF fd. struct Import { std::shared_ptr owner; VkBuffer buffer = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; ~Import() { vkDestroyBuffer(owner->device, buffer, nullptr); vkFreeMemory(owner->device, memory, nullptr); } }; std::mutex mutex; VkInstance instance = VK_NULL_HANDLE; VkPhysicalDevice physical = VK_NULL_HANDLE; VkDevice device = VK_NULL_HANDLE; VkQueue queue = VK_NULL_HANDLE; uint32_t family = 0; uint32_t max_buffer_range = 0; VkDescriptorSetLayout set_layout = VK_NULL_HANDLE; VkPipelineLayout pipeline_layout = VK_NULL_HANDLE; VkShaderModule shader = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE; VkShaderModule overlay_shader = VK_NULL_HANDLE; VkPipeline overlay_pipeline = VK_NULL_HANDLE; VkShaderModule transform_shader = VK_NULL_HANDLE; VkShaderModule vertical_shader = VK_NULL_HANDLE; std::map, std::array> transform_pipelines; VkBuffer scratch_buffer = VK_NULL_HANDLE; VkDeviceMemory scratch_memory = VK_NULL_HANDLE; VkDeviceSize scratch_size = 0; VkBuffer filter_buffer = VK_NULL_HANDLE; VkDeviceMemory filter_memory = VK_NULL_HANDLE; std::array filter_geometry{}; std::array filter_offsets{}; VkDescriptorPool descriptor_pool = VK_NULL_HANDLE; VkDescriptorSet descriptor_set = VK_NULL_HANDLE; VkCommandPool command_pool = VK_NULL_HANDLE; VkCommandBuffer commands = VK_NULL_HANDLE; VkFence fence = VK_NULL_HANDLE; PFN_vkGetMemoryFdPropertiesKHR get_fd_properties = nullptr; ~Impl() { if (device) { vkDestroyBuffer(device, scratch_buffer, nullptr); vkFreeMemory(device, scratch_memory, nullptr); for (const auto &entry : transform_pipelines) for (auto pipeline : entry.second) vkDestroyPipeline(device, pipeline, nullptr); vkDestroyShaderModule(device, vertical_shader, nullptr); vkDestroyBuffer(device, filter_buffer, nullptr); vkFreeMemory(device, filter_memory, nullptr); vkDestroyShaderModule(device, transform_shader, nullptr); vkDestroyFence(device, fence, nullptr); vkDestroyCommandPool(device, command_pool, nullptr); vkDestroyDescriptorPool(device, descriptor_pool, nullptr); vkDestroyPipeline(device, overlay_pipeline, nullptr); vkDestroyShaderModule(device, overlay_shader, nullptr); vkDestroyPipeline(device, pipeline, nullptr); vkDestroyShaderModule(device, shader, nullptr); vkDestroyPipelineLayout(device, pipeline_layout, nullptr); vkDestroyDescriptorSetLayout(device, set_layout, nullptr); vkDestroyDevice(device, nullptr); } vkDestroyInstance(instance, nullptr); } void initialize(dev_t render_device) { VkApplicationInfo application{}; application.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; application.pApplicationName = "qcom-va-driver"; application.apiVersion = VK_API_VERSION_1_1; VkInstanceCreateInfo instance_info{}; instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; instance_info.pApplicationInfo = &application; vk_check(vkCreateInstance(&instance_info, nullptr, &instance), "create instance"); uint32_t count = 0; vk_check(vkEnumeratePhysicalDevices(instance, &count, nullptr), "enumerate devices"); std::vector devices(count); vk_check(vkEnumeratePhysicalDevices(instance, &count, devices.data()), "enumerate devices"); const char *extensions[] = { VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME, VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME, VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME, VK_EXT_PHYSICAL_DEVICE_DRM_EXTENSION_NAME}; for (auto candidate : devices) { uint32_t extension_count = 0; vk_check( vkEnumerateDeviceExtensionProperties(candidate, nullptr, &extension_count, nullptr), "enumerate extensions"); std::vector available(extension_count); vk_check(vkEnumerateDeviceExtensionProperties(candidate, nullptr, &extension_count, available.data()), "enumerate extensions"); bool supported = true; for (auto name : extensions) supported &= std::any_of(available.begin(), available.end(), [&](const auto &e) { return std::strcmp(name, e.extensionName) == 0; }); if (!supported) continue; VkPhysicalDeviceDrmPropertiesEXT drm{}; drm.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRM_PROPERTIES_EXT; VkPhysicalDeviceProperties2 properties{}; properties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; properties.pNext = &drm; vkGetPhysicalDeviceProperties2(candidate, &properties); bool matches_render = drm.hasRender && drm.renderMajor == major(render_device) && drm.renderMinor == minor(render_device); bool matches_primary = drm.hasPrimary && drm.primaryMajor == major(render_device) && drm.primaryMinor == minor(render_device); if ((!matches_render && !matches_primary) || properties.properties.apiVersion < VK_API_VERSION_1_1) continue; VkPhysicalDevice16BitStorageFeatures storage{}; storage.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES; VkPhysicalDeviceFeatures2 features{}; features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; features.pNext = &storage; vkGetPhysicalDeviceFeatures2(candidate, &features); if (!storage.storageBuffer16BitAccess) continue; VkPhysicalDeviceExternalBufferInfo external{}; external.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_BUFFER_INFO; external.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; external.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; VkExternalBufferProperties external_properties{}; external_properties.sType = VK_STRUCTURE_TYPE_EXTERNAL_BUFFER_PROPERTIES; vkGetPhysicalDeviceExternalBufferProperties(candidate, &external, &external_properties); if (!(external_properties.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT)) continue; uint32_t family_count = 0; vkGetPhysicalDeviceQueueFamilyProperties(candidate, &family_count, nullptr); std::vector families(family_count); vkGetPhysicalDeviceQueueFamilyProperties(candidate, &family_count, families.data()); for (uint32_t i = 0; i < family_count; ++i) { if (!(families[i].queueFlags & VK_QUEUE_COMPUTE_BIT)) continue; physical = candidate; family = i; max_buffer_range = properties.properties.limits.maxStorageBufferRange; trace("Vulkan VPP: %s", properties.properties.deviceName); break; } if (physical) break; } check(physical, "Vulkan DMA-BUF compute unavailable", VA_STATUS_ERROR_UNSUPPORTED_FILTER); float priority = 1.0f; VkDeviceQueueCreateInfo queue_info{}; queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; queue_info.queueFamilyIndex = family; queue_info.queueCount = 1; queue_info.pQueuePriorities = &priority; VkPhysicalDevice16BitStorageFeatures storage{}; storage.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES; storage.storageBuffer16BitAccess = VK_TRUE; VkDeviceCreateInfo device_info{}; device_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; device_info.pNext = &storage; device_info.queueCreateInfoCount = 1; device_info.pQueueCreateInfos = &queue_info; device_info.enabledExtensionCount = std::size(extensions); device_info.ppEnabledExtensionNames = extensions; vk_check(vkCreateDevice(physical, &device_info, nullptr, &device), "create device"); vkGetDeviceQueue(device, family, 0, &queue); get_fd_properties = reinterpret_cast( vkGetDeviceProcAddr(device, "vkGetMemoryFdPropertiesKHR")); check(get_fd_properties, "Vulkan external memory entrypoint unavailable"); VkDescriptorSetLayoutBinding bindings[5]{}; for (unsigned i = 0; i < 5; ++i) bindings[i] = {i, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; VkDescriptorSetLayoutCreateInfo set_info{}; set_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; set_info.bindingCount = 5; set_info.pBindings = bindings; vk_check(vkCreateDescriptorSetLayout(device, &set_info, nullptr, &set_layout), "create descriptor layout"); VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(OverlayParams)}; VkPipelineLayoutCreateInfo layout_info{}; layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; layout_info.setLayoutCount = 1; layout_info.pSetLayouts = &set_layout; layout_info.pushConstantRangeCount = 1; layout_info.pPushConstantRanges = &push; vk_check(vkCreatePipelineLayout(device, &layout_info, nullptr, &pipeline_layout), "create pipeline layout"); VkShaderModuleCreateInfo shader_info{}; shader_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; shader_info.codeSize = sizeof(tonemap_spirv); shader_info.pCode = tonemap_spirv; vk_check(vkCreateShaderModule(device, &shader_info, nullptr, &shader), "create shader"); VkComputePipelineCreateInfo pipeline_info{}; pipeline_info.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; pipeline_info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; pipeline_info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; pipeline_info.stage.module = shader; pipeline_info.stage.pName = "main"; pipeline_info.layout = pipeline_layout; vk_check( vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &pipeline), "create pipeline"); shader_info.codeSize = sizeof(overlay_spirv); shader_info.pCode = overlay_spirv; vk_check(vkCreateShaderModule(device, &shader_info, nullptr, &overlay_shader), "create overlay shader"); pipeline_info.stage.module = overlay_shader; vk_check(vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &overlay_pipeline), "create overlay pipeline"); shader_info.codeSize = sizeof(transform_spirv); shader_info.pCode = transform_spirv; vk_check(vkCreateShaderModule(device, &shader_info, nullptr, &transform_shader), "create transform shader"); shader_info.codeSize = sizeof(vertical_spirv); shader_info.pCode = vertical_spirv; vk_check(vkCreateShaderModule(device, &shader_info, nullptr, &vertical_shader), "create vertical shader"); VkDescriptorPoolSize pool_size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 5}; VkDescriptorPoolCreateInfo pool_info{}; pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; pool_info.maxSets = 1; pool_info.poolSizeCount = 1; pool_info.pPoolSizes = &pool_size; vk_check(vkCreateDescriptorPool(device, &pool_info, nullptr, &descriptor_pool), "create descriptor pool"); VkDescriptorSetAllocateInfo set_allocate{}; set_allocate.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; set_allocate.descriptorPool = descriptor_pool; set_allocate.descriptorSetCount = 1; set_allocate.pSetLayouts = &set_layout; vk_check(vkAllocateDescriptorSets(device, &set_allocate, &descriptor_set), "allocate descriptors"); VkCommandPoolCreateInfo command_info{}; command_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; command_info.queueFamilyIndex = family; vk_check(vkCreateCommandPool(device, &command_info, nullptr, &command_pool), "create command pool"); VkCommandBufferAllocateInfo command_allocate{}; command_allocate.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; command_allocate.commandPool = command_pool; command_allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; command_allocate.commandBufferCount = 1; vk_check(vkAllocateCommandBuffers(device, &command_allocate, &commands), "allocate command buffer"); VkFenceCreateInfo fence_info{}; fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; vk_check(vkCreateFence(device, &fence_info, nullptr, &fence), "create fence"); } std::shared_ptr import(Memory &memory) { if (memory.vulkan_registration) { auto imported = std::static_pointer_cast(memory.vulkan_registration); check(imported->owner.get() == this, "DMA-BUF belongs to another Vulkan device"); return imported; } check(memory.size <= max_buffer_range, "Vulkan storage buffer exceeds device limit", VA_STATUS_ERROR_RESOLUTION_NOT_SUPPORTED); auto imported = std::make_shared(); imported->owner = shared_from_this(); VkExternalMemoryBufferCreateInfo external{}; external.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO; external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; VkBufferCreateInfo buffer_info{}; buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; buffer_info.pNext = &external; buffer_info.size = memory.size; buffer_info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; vk_check(vkCreateBuffer(device, &buffer_info, nullptr, &imported->buffer), "create external buffer"); VkMemoryRequirements requirements; vkGetBufferMemoryRequirements(device, imported->buffer, &requirements); check(requirements.size <= memory.size, "DMA-BUF too small for Vulkan alignment", VA_STATUS_ERROR_INVALID_SURFACE); VkMemoryFdPropertiesKHR properties{}; properties.sType = VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR; vk_check(get_fd_properties(device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, memory.fd, &properties), "query DMA-BUF memory"); uint32_t types = properties.memoryTypeBits & requirements.memoryTypeBits; check(types, "no compatible Vulkan DMA-BUF memory type", VA_STATUS_ERROR_INVALID_SURFACE); VkMemoryDedicatedAllocateInfo dedicated{}; dedicated.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO; dedicated.buffer = imported->buffer; VkImportMemoryFdInfoKHR fd_info{}; fd_info.sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR; fd_info.pNext = &dedicated; fd_info.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; fd_info.fd = dup(memory.fd); check(fd_info.fd >= 0, "duplicate Vulkan DMA-BUF fd", VA_STATUS_ERROR_ALLOCATION_FAILED); VkMemoryAllocateInfo allocation{}; allocation.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; allocation.pNext = &fd_info; allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = __builtin_ctz(types); VkResult result = vkAllocateMemory(device, &allocation, nullptr, &imported->memory); // Vulkan takes ownership of the duplicated fd only on successful import. if (result != VK_SUCCESS) close(fd_info.fd); vk_check(result, "import DMA-BUF"); vk_check(vkBindBufferMemory(device, imported->buffer, imported->memory, 0), "bind DMA-BUF"); memory.vulkan_registration = imported; return imported; } std::array transform_kernels(const TransformParameters &p) { auto taps = [](int input, int output) -> uint32_t { return input < output ? 4 : input % output || input / output > 4 ? 0 : input / output; }; unsigned mode = p.enlarging ? 2 : (p.src_width != p.scaled_width || p.src_height != p.scaled_height) ? 1 : 0; std::array key{uint32_t(p.src_wide), uint32_t(p.dst_wide), uint32_t(p.rotation), mode, taps(p.src_width, p.scaled_width), taps(p.src_height, p.scaled_height)}; // Large area footprints benefit from a shared vertical pass too. Keep // their tap loops dynamic to avoid unrolling hundreds of buffer loads. if (mode == 1 && (p.src_width > p.scaled_width * 4 || p.src_height > p.scaled_height * 4)) key[3] = mode = 3; auto &pipelines = transform_pipelines[key]; VkSpecializationMapEntry entries[6]; for (uint32_t i = 0; i < 6; ++i) entries[i] = {i, i * uint32_t(sizeof(uint32_t)), sizeof(uint32_t)}; VkSpecializationInfo specialization{6, entries, sizeof(key), key.data()}; for (unsigned i = 0; i < (mode >= 2 ? 2u : 1u); ++i) { if (pipelines[i]) continue; VkComputePipelineCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; info.layout = pipeline_layout; info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; info.stage.module = i ? vertical_shader : transform_shader; info.stage.pName = "main"; info.stage.pSpecializationInfo = &specialization; vk_check( vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &info, nullptr, &pipelines[i]), "create transform pipeline"); } return pipelines; } void allocate_buffer(VkBuffer &buffer, VkDeviceMemory &memory, VkDeviceSize size, VkMemoryPropertyFlags flags) { check(size <= max_buffer_range, "Vulkan internal buffer exceeds device limit", VA_STATUS_ERROR_RESOLUTION_NOT_SUPPORTED); vkDestroyBuffer(device, buffer, nullptr); vkFreeMemory(device, memory, nullptr); buffer = VK_NULL_HANDLE; memory = VK_NULL_HANDLE; VkBufferCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; info.size = size; info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; vk_check(vkCreateBuffer(device, &info, nullptr, &buffer), "create internal buffer"); VkMemoryRequirements requirements; vkGetBufferMemoryRequirements(device, buffer, &requirements); VkPhysicalDeviceMemoryProperties properties; vkGetPhysicalDeviceMemoryProperties(physical, &properties); unsigned index = properties.memoryTypeCount; for (unsigned i = 0; i < properties.memoryTypeCount; ++i) if ((requirements.memoryTypeBits & (1u << i)) && (properties.memoryTypes[i].propertyFlags & flags) == flags) { index = i; break; } check(index < properties.memoryTypeCount, "Vulkan buffer memory type unavailable"); VkMemoryAllocateInfo allocation{}; allocation.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = index; vk_check(vkAllocateMemory(device, &allocation, nullptr, &memory), "allocate internal buffer"); vk_check(vkBindBufferMemory(device, buffer, memory, 0), "bind internal buffer"); } void prepare_filters(int sw, int sh, int dw, int dh) { std::array geometry{sw, sh, dw, dh}; if (geometry == filter_geometry) return; std::vector words; for (int axis = 0; axis < 4; ++axis) { int channels = axis >= 2 ? 2 : 1; int input = (axis & 1 ? sh : sw) / channels; int output = (axis & 1 ? dh : dw) / channels; filter_offsets[axis] = words.size(); for (int i = 0; i < output; ++i) { auto filter = make_filter(input, output, i); words.push_back(filter.tap_count); words.push_back(filter.shift); for (int tap = 0; tap < FILTER_MAX_TAPS; ++tap) words.push_back(uint32_t(filter.indices[tap]) | (uint32_t(uint16_t(filter.weights[tap])) << 16)); } } // Only coefficients are CPU mapped. Input/output pixels remain in their // original DMA-BUFs, and unchanged geometry reuses the coefficient buffer. filter_geometry = {}; VkDeviceSize size = words.size() * sizeof(uint32_t); allocate_buffer(filter_buffer, filter_memory, size, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); void *mapped; vk_check(vkMapMemory(device, filter_memory, 0, size, 0, &mapped), "map filters"); std::memcpy(mapped, words.data(), size); vkUnmapMemory(device, filter_memory); filter_geometry = geometry; } void dispatch(VkPipeline selected_pipeline, Memory *src, Memory *dst, Memory *rgba, const void *parameters, uint32_t parameter_size, unsigned groups_x, unsigned groups_y, bool filtering = false, VkPipeline vertical = VK_NULL_HANDLE, unsigned vertical_x = 0, unsigned vertical_y = 0) { Memory *memories[] = {src, dst, rgba ? rgba : src}; VkDescriptorBufferInfo buffers[5]{}; VkWriteDescriptorSet writes[5]{}; for (unsigned i = 0; i < 5; ++i) { if (i < 3) { wait_surface_access(*memories[i], i == 1 ? POLLOUT : POLLIN); buffers[i] = {import(*memories[i])->buffer, 0, memories[i]->size}; } else if (i == 3) buffers[i] = filtering ? VkDescriptorBufferInfo{filter_buffer, 0, VK_WHOLE_SIZE} : buffers[0]; else buffers[i] = vertical_x ? VkDescriptorBufferInfo{scratch_buffer, 0, VK_WHOLE_SIZE} : buffers[0]; writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = descriptor_set; writes[i].dstBinding = i; writes[i].descriptorCount = 1; writes[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; writes[i].pBufferInfo = &buffers[i]; } vkUpdateDescriptorSets(device, 5, writes, 0, nullptr); vk_check(vkResetCommandPool(device, command_pool, 0), "reset command pool"); VkCommandBufferBeginInfo begin{}; begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; vk_check(vkBeginCommandBuffer(commands, &begin), "begin command buffer"); // DMA-BUF producers/consumers are outside this Vulkan queue family. Import // alone does not acquire ownership or replace their implicit fences. VkBufferMemoryBarrier barriers[3]{}; unsigned barrier_count = 0; for (unsigned i = 0; i < 3; ++i) { if (i && buffers[i].buffer == buffers[0].buffer) { if (i == 1) barriers[0].dstAccessMask |= VK_ACCESS_SHADER_WRITE_BIT; continue; } unsigned j = barrier_count++; barriers[j].sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; barriers[j].dstAccessMask = i == 1 ? VK_ACCESS_SHADER_WRITE_BIT : VK_ACCESS_SHADER_READ_BIT; barriers[j].srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT; barriers[j].dstQueueFamilyIndex = family; barriers[j].buffer = buffers[i].buffer; barriers[j].size = VK_WHOLE_SIZE; } vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, barrier_count, barriers, 0, nullptr); vkCmdBindDescriptorSets(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline_layout, 0, 1, &descriptor_set, 0, nullptr); vkCmdPushConstants(commands, pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, parameter_size, parameters); if (vertical_x) { vkCmdBindPipeline(commands, VK_PIPELINE_BIND_POINT_COMPUTE, vertical); vkCmdDispatch(commands, vertical_x, vertical_y, 1); VkBufferMemoryBarrier scratch{}; scratch.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; scratch.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; scratch.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; scratch.srcQueueFamilyIndex = scratch.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; scratch.buffer = scratch_buffer; scratch.size = VK_WHOLE_SIZE; vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 1, &scratch, 0, nullptr); } vkCmdBindPipeline(commands, VK_PIPELINE_BIND_POINT_COMPUTE, selected_pipeline); vkCmdDispatch(commands, groups_x, groups_y, 1); for (unsigned i = 0; i < barrier_count; ++i) { auto &barrier = barriers[i]; barrier.srcAccessMask = barrier.dstAccessMask; barrier.dstAccessMask = 0; barrier.srcQueueFamilyIndex = family; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT; } vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, barrier_count, barriers, 0, nullptr); vk_check(vkEndCommandBuffer(commands), "end command buffer"); vk_check(vkResetFences(device, 1, &fence), "reset fence"); VkSubmitInfo submit{}; submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submit.commandBufferCount = 1; submit.pCommandBuffers = &commands; vk_check(vkQueueSubmit(queue, 1, &submit, fence), "submit VPP"); // Complete before returning buffers to CDSP, V4L2, or the VA caller. No CPU // mapping or pixel copy is involved; imports persist for the Memory lifetime. vk_check(vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX), "wait for VPP"); publish_dma_buf(*memories[1]); } }; VulkanVpp::VulkanVpp(int render_fd) { struct stat info{}; check(fstat(render_fd, &info) == 0 && S_ISCHR(info.st_mode), "invalid Vulkan render device"); static std::mutex cache_mutex; static std::map> cache; std::lock_guard guard(cache_mutex); impl_ = cache[info.st_rdev].lock(); if (!impl_) { impl_ = std::make_shared(); impl_->initialize(info.st_rdev); cache[info.st_rdev] = impl_; } } void VulkanVpp::process(const VppPicture &picture, Surface &target) { std::lock_guard guard(impl_->mutex); auto &src = *picture.source->memory; auto &dst = *target.memory; validate_layout(src, *picture.source); validate_layout(dst, target); auto crop = picture.region; double source_pq = encode_pq(picture.source_peak); trace("Vulkan tone mapping %ux%u peak=%g src=%u/%u/%u dst=%u/%u/%u", crop.width, crop.height, picture.source_peak, src.stride, src.data_offset, src.storage_height, dst.stride, dst.data_offset, dst.storage_height); float destination = encode_pq(100.0) / source_pq; Parameters params{crop.width, crop.height, src.stride, src.data_offset + unsigned(crop.y) * src.stride + unsigned(crop.x) * 2, src.data_offset + src.storage_height * src.stride + unsigned(crop.y / 2) * src.stride + unsigned(crop.x) * 2, dst.stride, dst.data_offset, dst.data_offset + dst.storage_height * dst.stride, float(source_pq), destination, 1.5f * destination - 0.5f}; impl_->dispatch(impl_->pipeline, &src, &dst, nullptr, ¶ms, sizeof(params), (crop.width + 255) / 256, crop.height / 2); } void VulkanVpp::overlay(Surface &base, Surface &rgba, Surface &target, const OverlayParams ¶ms) { std::lock_guard guard(impl_->mutex); validate_layout(*base.memory, base); validate_layout(*target.memory, target); impl_->dispatch(impl_->overlay_pipeline, base.memory.get(), target.memory.get(), rgba.memory.get(), ¶ms, sizeof(params), (target.width + 127) / 128, target.height / 2); } void VulkanVpp::transform(const VppPicture &picture, Surface &target) { std::lock_guard guard(impl_->mutex); auto &src = *picture.source->memory, &dst = *target.memory; validate_layout(src, *picture.source); validate_layout(dst, target); check(src.size <= INT32_MAX && dst.size <= INT32_MAX, "Vulkan transform buffer exceeds signed offset range", VA_STATUS_ERROR_INVALID_SURFACE); TransformParameters p{}; auto crop = picture.region; p.src_width = crop.width; p.src_height = crop.height; p.width = target.width; p.height = target.height; p.scaled_width = picture.rotation & 1 ? target.height : target.width; p.scaled_height = picture.rotation & 1 ? target.width : target.height; p.src_wide = picture.source->fourcc == VA_FOURCC_P010; p.dst_wide = target.fourcc == VA_FOURCC_P010; p.rotation = picture.rotation; p.enlarging = crop.width < p.scaled_width || crop.height < p.scaled_height; p.src_stride = src.stride; p.src_y = src.data_offset + crop.y * src.stride + crop.x * (1 + p.src_wide); p.src_uv = src.data_offset + src.stride * (src.storage_height + crop.y / 2) + crop.x * (1 + p.src_wide); p.dst_stride = dst.stride; p.dst_y = dst.data_offset; p.dst_uv = dst.data_offset + dst.stride * dst.storage_height; bool scaling = p.src_width != p.scaled_width || p.src_height != p.scaled_height; if (scaling) { impl_->prepare_filters(p.src_width, p.src_height, p.scaled_width, p.scaled_height); p.filter_x = impl_->filter_offsets[0]; p.filter_y = impl_->filter_offsets[1]; p.filter_uv_x = impl_->filter_offsets[2]; p.filter_uv_y = impl_->filter_offsets[3]; } auto pipelines = impl_->transform_kernels(p); if (pipelines[1]) { VkDeviceSize size = VkDeviceSize(p.src_width) * p.scaled_height * 3; if (size > impl_->scratch_size) { impl_->scratch_size = 0; impl_->allocate_buffer(impl_->scratch_buffer, impl_->scratch_memory, size, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); impl_->scratch_size = size; } } impl_->dispatch(pipelines[0], &src, &dst, nullptr, &p, sizeof(p), (target.width + 15) / 16, (target.height + 15) / 16, scaling, pipelines[1], pipelines[1] ? (p.src_width + 15) / 16 : 0, (p.scaled_height + 15) / 16); trace("VPP Vulkan %ux%u -> %ux%u rotation=%u", crop.width, crop.height, target.width, target.height, picture.rotation * 90); } } // namespace irisva qcom-va-driver-1.1.0/src/vulkan/vpp.hpp000066400000000000000000000010001526764642600177630ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #pragma once #include #include "../overlay-params.h" namespace irisva { struct Surface; struct VppPicture; class VulkanVpp { struct Impl; std::shared_ptr impl_; public: explicit VulkanVpp(int render_fd); void process(const VppPicture &picture, Surface &target); void transform(const VppPicture &picture, Surface &target); void overlay(Surface &base, Surface &rgba, Surface &target, const OverlayParams ¶ms); }; } // namespace irisva qcom-va-driver-1.1.0/subprojects/000077500000000000000000000000001526764642600167325ustar00rootroot00000000000000qcom-va-driver-1.1.0/subprojects/fastrpc.wrap000066400000000000000000000005061526764642600212700ustar00rootroot00000000000000[wrap-file] directory = fastrpc-a56e9d4de3614a8d8f4b0e15ff561350c4b686e3 source_url = https://codeload.github.com/qualcomm/fastrpc/tar.gz/a56e9d4de3614a8d8f4b0e15ff561350c4b686e3 source_filename = fastrpc-a56e9d4d.tar.gz source_hash = 9cae90624cd22ed6c013007f1eaaca0555903ebf91f2b3f16797985e09392c86 patch_directory = fastrpc qcom-va-driver-1.1.0/subprojects/hexagon.wrap000066400000000000000000000006611526764642600212610ustar00rootroot00000000000000[wrap-file] directory = hexagon-open-access-19.0.02 source_url = https://softwarecenter.qualcomm.com/api/download/software/tools/Hexagon_open_access/Linux/ARM64/Debian/19.0.02/Hexagon_open_access.Core.19.0.02.Linux-ARM64.tar.gz source_filename = Hexagon_open_access.Core.19.0.02.Linux-ARM64.tar.gz source_hash = 82f49d8e7e79402eb6fd71f2021e8714f15fc6564234de973daf0e4a962ce9ed lead_directory_missing = true patch_directory = hexagon qcom-va-driver-1.1.0/subprojects/packagefiles/000077500000000000000000000000001526764642600213505ustar00rootroot00000000000000qcom-va-driver-1.1.0/subprojects/packagefiles/fastrpc/000077500000000000000000000000001526764642600230125ustar00rootroot00000000000000qcom-va-driver-1.1.0/subprojects/packagefiles/fastrpc/meson.build000066400000000000000000000044061526764642600251600ustar00rootroot00000000000000project('fastrpc', 'c', version: 'a56e9d4d', license: 'BSD-3-Clause', default_options: ['warning_level=0', 'werror=false', 'c_std=gnu11']) cc = meson.get_compiler('c') fastrpc_include_dir = meson.current_source_dir() / 'inc' fastrpc_includes = include_directories('inc') rpc = shared_library('iris_cdsprpc', 'src/fastrpc_apps_user.c', 'src/fastrpc_perf.c', 'src/fastrpc_pm.c', 'src/fastrpc_config.c', 'src/fastrpc_mem.c', 'src/fastrpc_notif.c', 'src/fastrpc_ioctl.c', 'src/fastrpc_log.c', 'src/fastrpc_procbuf.c', 'src/fastrpc_cap.c', 'src/log_config.c', 'src/dspsignal.c', 'src/dspqueue/dspqueue_cpu.c', 'src/dspqueue/dspqueue_rpc_stub.c', 'src/listener_android.c', 'src/apps_std_imp.c', 'src/apps_mem_imp.c', 'src/apps_mem_skel.c', 'src/rpcmem_linux.c', 'src/adspmsgd.c', 'src/adspmsgd_printf.c', 'src/std_path.c', 'src/std_dtoa.c', 'src/BufBound.c', 'src/platform_libs.c', 'src/pl_list.c', 'src/gpls.c', 'src/remotectl_stub.c', 'src/remotectl1_stub.c', 'src/adspmsgd_apps_skel.c', 'src/adspmsgd_adsp_stub.c', 'src/adspmsgd_adsp1_stub.c', 'src/apps_remotectl_skel.c', 'src/adsp_current_process_stub.c', 'src/adsp_current_process1_stub.c', 'src/adsp_listener_stub.c', 'src/adsp_listener1_stub.c', 'src/apps_std_skel.c', 'src/adsp_perf_stub.c', 'src/adsp_perf1_stub.c', 'src/mod_table.c', 'src/fastrpc_context.c', 'src/fastrpc_config_parser.c', include_directories: fastrpc_includes, dependencies: [dependency('threads'), dependency('yaml-0.1'), dependency('libbsd-overlay'), cc.find_library('dl'), cc.find_library('m')], c_args: ['-fno-short-enums', '-U_DEBUG', '-DARM_ARCH_7A', '-DLE_ENABLE', '-DENABLE_UPSTREAM_DRIVER_INTERFACE', '-DUSE_SYSLOG', '-DPARSE_YAML', '-DDEFAULT_DOMAIN_ID=3', '-DCONFIG_BASE_DIR="/usr/share/qcom/"', '-DMACHINE_NAME_PATH="/sys/firmware/devicetree/base/model"', '-DDEFAULT_DSP_SEARCH_PATHS=";/usr/lib/rfsa/adsp;/usr/lib/dsp;"'], override_options: ['werror=false'], install: true, install_dir: get_option('libdir') / 'qcom-va-driver') fastrpc_dep = declare_dependency(link_with: rpc, include_directories: fastrpc_includes) install_data('LICENSE.txt', install_dir: get_option('datadir') / 'licenses/qcom-va-driver/fastrpc') qcom-va-driver-1.1.0/subprojects/packagefiles/hexagon/000077500000000000000000000000001526764642600230015ustar00rootroot00000000000000qcom-va-driver-1.1.0/subprojects/packagefiles/hexagon/meson.build000066400000000000000000000002001526764642600251330ustar00rootroot00000000000000project('hexagon-open-access', version: '19.0.02', license: 'proprietary') hexagon_tools = meson.current_source_dir() / 'Tools' qcom-va-driver-1.1.0/subprojects/packagefiles/qaic/000077500000000000000000000000001526764642600222655ustar00rootroot00000000000000qcom-va-driver-1.1.0/subprojects/packagefiles/qaic/build.py000066400000000000000000000026241526764642600237420ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT """Build the pinned open QAIC with system GHC packages, without Cabal downloads.""" import json from pathlib import Path import shutil import subprocess import sys ghc, source, output = sys.argv[1:] output = Path(output).resolve() work = output.parent / 'qaic-source' shutil.copytree(Path(source) / 'src', work, dirs_exist_ok=True) version = work / 'Compiler/Version.hs' text = version.read_text() for name, value in { 'gitcommit': 'a66a0019fb488abae955bbc9b3a9de3dc15962d0', 'ghcVersion': subprocess.check_output([ghc, '--numeric-version'], text=True).strip(), 'cabalVersion': 'built directly with GHC', 'platform': sys.platform, }.items(): text += f'\n{name} :: String\n{name} = {json.dumps(value)}\n' version.write_text(text) def run(args, **kw): subprocess.run(args, cwd=work, check=True, **kw) converter = work / 'file2hs-tool' run([ghc, '-dynamic', '-O2', '-outputdir', 'build/file2hs', '-o', str(converter), 'file2hs/file2hs.hs']) for name in ['Stub', 'Skel', 'CStub', 'Env', 'Allocator', 'Wstring', 'Slim']: base = work / 'Language/SO' / name with base.with_suffix('.c').open('rb') as src, base.with_suffix('.hs').open('wb') as dst: run([str(converter), 'Language.SO.' + name, 'source'], stdin=src, stdout=dst) run([ghc, '--make', '-dynamic', '-O2', '-threaded', '-outputdir', 'build/qaic', '-o', str(output), 'Qidlc.hs']) qcom-va-driver-1.1.0/subprojects/packagefiles/qaic/meson.build000066400000000000000000000007531526764642600244340ustar00rootroot00000000000000project('qaic', version: '01.00.51', license: 'BSD-3-Clause-Clear') python = import('python').find_installation() ghc = find_program('ghc', native: true) qaic = custom_target('qaic', output: 'qaic', command: [python, files('build.py'), ghc, meson.current_source_dir(), '@OUTPUT@'], depend_files: files('build.py'), build_by_default: false) qaic_idl_dir = meson.current_source_dir() / 'inc' install_data('LICENSE.txt', install_dir: get_option('datadir') / 'licenses/qcom-va-driver/qaic') qcom-va-driver-1.1.0/subprojects/qaic.wrap000066400000000000000000000004721526764642600205450ustar00rootroot00000000000000[wrap-file] directory = QAIC-a66a0019fb488abae955bbc9b3a9de3dc15962d0 source_url = https://codeload.github.com/qualcomm/QAIC/tar.gz/a66a0019fb488abae955bbc9b3a9de3dc15962d0 source_filename = qaic-a66a0019.tar.gz source_hash = 7f033a57adee248962a4317b8b4367060bdef1e7fb50fac403e7e21e2552f5af patch_directory = qaic qcom-va-driver-1.1.0/tests/000077500000000000000000000000001526764642600155315ustar00rootroot00000000000000qcom-va-driver-1.1.0/tests/README.md000066400000000000000000000152131526764642600170120ustar00rootroot00000000000000# Hardware regression tests Run on the supported Iris board. These are hardware integration tests, not host unit tests. C/C++ files include build commands in their headers; FFmpeg tests also require `ffprobe` and software H.264/HEVC decoding. `meson test -C build` also checks CAPTURE buffer budgeting without hardware. | Test | Coverage | | --- | --- | | `decoder/main10.cpp` | HEVC Main10 / AV1 8/10-bit capabilities, config validation, surface enumeration, and context bit-depth selection. | | `decoder/av1.py` | FFmpeg/GStreamer AV1 pixels against dav1d: 720p–4K, 8/10-bit, hidden references, tiles, lossless, still pictures, super-resolution and global motion. | | `decoder/prime-import.cpp` | PRIME1/PRIME2 NV12/P010 imports, malformed descriptors, shared storage, and caller FD ownership. | | `decoder/error-recovery.py` | Corrupt HEVC, queue errors, neutral NV12/P010 pixels, B frames, consecutive errors, DRM mapping, and persistent/imported storage. | | `encoder/ffmpeg-smoke.py` | Profiles, rate control, dimensions, frame counts, keyframes, and decoded quality. | | `encoder/api-smoke.cpp` | Uploads, direct/staged input, rejected requests, coded buffers, and resource cleanup. | | `encoder/async.cpp` | Queued versus serial output, QP/IDR changes, non-IDR I requests, synchronization, timeouts, and pending resource destruction. | | `encoder/writable-export.cpp` | GPU-written input, stable DMA-BUF exports, and GPU fence synchronization across non-IDR GOPs. | To select an uninstalled driver before running a test: ```sh export LIBVA_DRIVER_NAME=v4l2 LIBVA_DRIVERS_PATH="$PWD/build" ``` The PRIME import test requires `/dev/dma_heap/system`. It checks padded layouts and nonzero offsets, and verifies that importing and exporting retain the same DMA-BUF rather than copying its pixels. Run AV1 on a board exposing the V4L2 AV1 format, with the NOSHOW kernel patch: `python3 tests/decoder/av1.py tmp/av1 --driver-dir build --gstreamer --extended`. Requires FFmpeg with libaom/dav1d; `--gstreamer` also needs the `va` plugin and GStreamer app/video development libraries. `--extended` adds coding-tool cases and requires `aomenc`. Passing decoded files are removed; logs and streams remain. Run error recovery with `python3 tests/decoder/error-recovery.py tmp/recovery --driver-dir build` on a kernel with the error-timestamp patch. It generates HEVC samples with libx265 and builds the adjacent preload shims. Besides real corruption, it checks that injected errors replace only the affected frames, exports retain their DMA-BUF, and failed fills or missing timestamps do not report successful recovery. The FFmpeg, async, and writable-export tests accept `--hevc` to exercise HEVC Main instead of H.264. The HEVC async test also checks rejected picture types, tiles, partial slices, invalid QP/filter values, and long-term references. Place generated streams and logs under `tmp/`. The async and writable-export programs take an existing output directory as their first argument. Optional failure injection helpers (build commands are in their headers): - `reject-coherent.c`: load with `LD_PRELOAD` to test the CPU/MMAP fallback on hardware that normally supports coherent GEM. Works with all encoder tests. - `delay-completion.c`: load with `LD_PRELOAD` and set `IRIS_TEST_DELAY_COMPLETION=1` when running `async` to test caller timeouts and the hardware wait watchdog. - `decoder/error-completion.c`: load with `LD_PRELOAD` and set `IRIS_TEST_ERROR_QUEUE=capture`, `output`, or `both` to inject errors at decode token 46. `IRIS_TEST_ERROR_FIRST` and `IRIS_TEST_ERROR_COUNT` select another frame or consecutive errors. Errors must complete without a sync or context destruction timeout, including late CAPTURE after OUTPUT errors. For staged async input, set `IRIS_TEST_STAGING=1`; add `IRIS_VAAPI_COPY=cpu` to exercise CPU staging. Without that override, staging uses the build's default copy path. ## Build options `python3 tests/build/gpu-copy.py tmp/build-options` runs without Iris hardware. Install all GPU-copy development dependencies first. It masks each dependency in turn and checks `auto`, `enabled`, and `disabled`, source selection, linked libraries, and driver loading. Logs and builds remain in the output directory. On the board, also run the decoder recovery and encoder staging tests against `-Dgpu_copy=disabled` without setting `IRIS_VAAPI_COPY`, to exercise the default CPU path. Explicit `IRIS_VAAPI_COPY=gpu` must fail only when a copy is needed; DMA-BUF zero-copy remains usable. ## CDSP VPP `meson test -C build --print-errorlogs` checks MSM allocation cache modes, skeleton selection and HVX worker limits without hardware. Build with CDSP enabled, then run on the target (upstream FastRPC kernel ABI): ```sh c++ -O2 -std=c++17 -rdynamic tests/vpp/scaling.cpp -o build/vpp-scaling \ $(pkg-config --cflags --libs libva libva-drm libdrm vulkan) -ldl \ -Lbuild/src/cdsp -liris_vpp export LIBVA_DRIVER_NAME=v4l2 LIBVA_DRIVERS_PATH="$PWD/build" export LD_LIBRARY_PATH="$PWD/build/src/cdsp${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" export ADSP_LIBRARY_PATH="$PWD/build/src/cdsp${ADSP_LIBRARY_PATH:+;$ADSP_LIBRARY_PATH}" ./build/vpp-scaling ``` Checks NV12/P010 scaling, conversion and rotation, rounding, crop/output layouts, DMA-BUF identity, persistent mapping, context/buffer lifetime and rotation with five live contexts sharing scratch resources. Also run against a `-Dfastrpc=bundled` build. Pass `vulkan` to run the same VAAPI cases and check persistent Vulkan imports instead of FastRPC mappings. For a build without CDSP, add `-DVPP_NO_CDSP` and omit `-Lbuild/src/cdsp -liris_vpp`. `--bench` measures 100 synchronous submissions after a one-second warmup, excluding uploads and reference generation. `vpp/tonemap.cpp` checks Vulkan HDR10 → SDR against a double-precision reference, including metadata changes, padded/cropped layouts, DMA-BUF identity, persistent imports and context lifetime. Build with the command in its header and run with the same driver environment; `--geometry` also checks combined HDR, scaling and rotation with the selected VPP backend. It reports warmed VAAPI call times for 1080p and 4K. `vpp/overlay.cpp` checks both overlay backends against a double-precision reference, including RGB upload/PRIME round trips, clipping, scaling, alpha, multiple layers, rejected submissions and parameter lifetimes. Build with the command in its header. `--geometry` adds combined base scaling; `--bench` measures warmed VAAPI submissions including the base copy, excluding uploads and encoding. Pass `cdsp` or `vulkan` to test a single backend. Optional ABI audit against SDK 6.7 (all six architectures): ```sh python3 tests/vpp/check-abi.py /path/to/19.0.02/Tools /path/to/HexagonSDK6.7 ``` qcom-va-driver-1.1.0/tests/build/000077500000000000000000000000001526764642600166305ustar00rootroot00000000000000qcom-va-driver-1.1.0/tests/build/gpu-copy.py000066400000000000000000000102241526764642600207440ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT """Build-option regression; requires all GPU-copy development dependencies. Usage: python3 tests/build/gpu-copy.py tmp/build-options Masks each optional dependency in turn without changing installed packages. """ import argparse import ctypes import os from pathlib import Path import re import shutil import subprocess import sys import tempfile GPU_PACKAGES = {'egl', 'glesv2', 'gbm'} def pkg_config(): requested = GPU_PACKAGES.intersection( word for argument in sys.argv[2:] for word in argument.split()) with open(os.environ['GPU_COPY_TEST_QUERIES'], 'a') as log: log.write(' '.join(sorted(requested)) + '\n') if requested.intersection(os.environ['GPU_COPY_TEST_MISSING'].split(',')): return 1 return subprocess.run([os.environ['GPU_COPY_TEST_PKG_CONFIG'], *sys.argv[2:]]).returncode def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('directory', type=Path) args = parser.parse_args() args.directory.mkdir(parents=True, exist_ok=True) root = Path(tempfile.mkdtemp(prefix='gpu-copy-', dir=args.directory.resolve())) source = Path(__file__).resolve().parents[2] pkg = shutil.which('pkg-config') subprocess.run([pkg, '--exists', 'libva >= 1.10', 'libdrm', 'egl', 'glesv2 >= 3.2', 'gbm'], check=True) native = root / 'native.ini' # Block CMake fallback as well, so hidden pkg-config packages stay unavailable. native.write_text('[binaries]\npkg-config = ' + repr([ sys.executable, str(Path(__file__).resolve()), '--pkg-config']) + '\ncmake = ' + repr(shutil.which('false')) + '\n') ctypes.CDLL('libva.so.2', mode=os.RTLD_NOW | os.RTLD_GLOBAL) cases = [('disabled', ''), ('disabled', 'egl,glesv2,gbm'), ('auto', ''), ('enabled', '')] cases += [(mode, missing) for missing in ['egl', 'glesv2', 'gbm', 'egl,glesv2,gbm'] for mode in ['auto', 'enabled']] for mode, missing in cases: name = mode + '-' + (missing.replace(',', '-') or 'available') build = root / name queries = root / (name + '.queries') env = dict(os.environ, GPU_COPY_TEST_QUERIES=str(queries), GPU_COPY_TEST_MISSING=missing, GPU_COPY_TEST_PKG_CONFIG=pkg) with (root / (name + '.log')).open('w') as log: setup = subprocess.run(['meson', 'setup', str(build), str(source), '--native-file', str(native), '-Dgpu_copy=' + mode, '-Dcdsp_vpp=disabled', '-Dwerror=true'], env=env, stdout=log, stderr=subprocess.STDOUT) if mode == 'enabled' and missing: assert setup.returncode != 0, name assert missing.split(',')[0] in queries.read_text(), name print(f'PASS {name}: configuration rejected', flush=True) continue assert setup.returncode == 0, f'{name}: see {log.name}' subprocess.run(['meson', 'compile', '-C', str(build), '-j', '2'], env=env, stdout=log, stderr=subprocess.STDOUT, check=True) driver = build / 'v4l2_drv_video.so' dynamic = subprocess.check_output(['readelf', '-d', str(driver)], text=True) needed = re.findall(r'\(NEEDED\).*\[(.*?)\]', dynamic) gpu = mode != 'disabled' and not missing for library in ['libEGL.so', 'libGLESv2.so', 'libgbm.so']: assert any(n.startswith(library) for n in needed) == gpu, (name, needed) commands = (build / 'compile_commands.json').read_text() assert ('gpu-copy.cpp' in commands) == gpu, name assert ('cpu-copy.cpp' in commands) != gpu, name if mode == 'disabled': assert not queries.read_text().strip(), name module = ctypes.CDLL(str(driver), mode=os.RTLD_NOW) getattr(module, '__vaDriverInit_1_0') print(f'PASS {name}: build, dependencies, source selection, driver load', flush=True) print(f'Logs: {root}', flush=True) if __name__ == '__main__': if len(sys.argv) > 1 and sys.argv[1] == '--pkg-config': sys.exit(pkg_config()) main() qcom-va-driver-1.1.0/tests/decoder/000077500000000000000000000000001526764642600171365ustar00rootroot00000000000000qcom-va-driver-1.1.0/tests/decoder/av1-download.cpp000066400000000000000000000073761526764642600221530ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Used by av1.py to download VA frames with their actual plane strides. #include #include #include #include #include #include int main(int argc, char **argv) { gst_init(&argc, &argv); if (argc != 3) return 2; setvbuf(stdout, nullptr, _IOLBF, 0); GError *error = nullptr; GstElement *pipeline = gst_parse_launch(argv[1], &error); if (!pipeline || error) { fprintf(stderr, "pipeline: %s\n", error ? error->message : "null"); return 1; } auto *sink = GST_APP_SINK(gst_bin_get_by_name(GST_BIN(pipeline), "output")); if (!sink) return 2; auto *bus = gst_element_get_bus(pipeline); FILE *file = fopen(argv[2], "wb"); if (!file) return 1; gst_element_set_state(pipeline, GST_STATE_PLAYING); int count = 0, result = 0; auto last = std::chrono::steady_clock::now(); for (;;) { auto *sample = gst_app_sink_try_pull_sample(sink, GST_SECOND); if (!sample) { auto *message = gst_bus_pop_filtered(bus, GST_MESSAGE_ERROR); if (message) { gchar *debug = nullptr; gst_message_parse_error(message, &error, &debug); fprintf(stderr, "GStreamer error: %s / %s\n", error->message, debug ? debug : ""); gst_message_unref(message); result = 1; break; } if (gst_app_sink_is_eos(sink)) break; if (std::chrono::steady_clock::now() - last > std::chrono::seconds(10)) { fprintf(stderr, "timeout\n"); result = 1; break; } continue; } last = std::chrono::steady_clock::now(); GstVideoInfo info{}; GstVideoFrame frame{}; auto *buffer = gst_sample_get_buffer(sample); if (!gst_video_info_from_caps(&info, gst_sample_get_caps(sample)) || !gst_video_frame_map(&frame, &info, buffer, GST_MAP_READ)) { fprintf(stderr, "video frame mapping failed\n"); result = 1; gst_sample_unref(sample); break; } auto format = GST_VIDEO_FRAME_FORMAT(&frame); unsigned w = GST_VIDEO_FRAME_WIDTH(&frame), h = GST_VIDEO_FRAME_HEIGHT(&frame); if (format != GST_VIDEO_FORMAT_NV12 && format != GST_VIDEO_FORMAT_P010_10LE) { fprintf(stderr, "unexpected format\n"); result = 1; gst_video_frame_unmap(&frame); gst_sample_unref(sample); break; } unsigned bytes = w * (format == GST_VIDEO_FORMAT_NV12 ? 1 : 2); printf("frame=%d pts=%llu size=%zu %ux%u strides=%d,%d offsets=%zu,%zu flags=%x\n", count, (unsigned long long)GST_BUFFER_PTS(buffer), gst_buffer_get_size(buffer), w, h, frame.info.stride[0], frame.info.stride[1], frame.info.offset[0], frame.info.offset[1], GST_BUFFER_FLAGS(buffer)); for (unsigned plane = 0; plane < 2; ++plane) { auto *data = static_cast(GST_VIDEO_FRAME_PLANE_DATA(&frame, plane)); for (unsigned y = 0; y < (plane ? h / 2 : h); ++y) if (fwrite(data + size_t(y) * GST_VIDEO_FRAME_PLANE_STRIDE(&frame, plane), 1, bytes, file) != bytes) result = 1; } gst_video_frame_unmap(&frame); gst_sample_unref(sample); ++count; } gst_element_set_state(pipeline, GST_STATE_NULL); gst_object_unref(sink); gst_object_unref(bus); gst_object_unref(pipeline); fclose(file); printf("RESULT frames=%d exit=%d\n", count, result); return result; } qcom-va-driver-1.1.0/tests/decoder/av1.py000066400000000000000000000162661526764642600202120ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT """AV1 VAAPI decoding against dav1d; FFmpeg and optionally GStreamer.""" import argparse import hashlib import json import os from pathlib import Path import re import shlex import subprocess CASES = [ ('720p8', 1280, 720, 8, 60, 0), ('1080p8', 1920, 1080, 8, 60, 0), ('1080p10', 1920, 1080, 10, 60, 0), ('4k8', 3840, 2160, 8, 30, 0), ('4k10', 3840, 2160, 10, 30, 0), ('1080p8-lookahead', 1920, 1080, 8, 60, 25), ('1080p10-lookahead', 1920, 1080, 10, 60, 25), ] EXTENDED_CASES = [ ('tiles8', 1280, 720, 8, 30, 25), ('tiles10', 1280, 720, 10, 30, 25), ('lossless8', 1280, 720, 8, 12, 0), ('still10', 1280, 720, 10, 1, 0), ('superres8', 1280, 720, 8, 12, 0), ('motion8', 1280, 720, 8, 30, 0), ] ENCODER_OPTIONS = { 'tiles8': ['-tiles', '2x2'], 'tiles10': ['-tiles', '2x2'], 'lossless8': ['-aom-params', 'lossless=1'], 'still10': ['-still-picture', '1'], 'motion8': ['-cpu-used', '4', '-enable-global-motion', '1', '-aq-mode', '1'], } def digest(path): with path.open('rb') as file: result = hashlib.sha256() for chunk in iter(lambda: file.read(1024 * 1024), b''): result.update(chunk) return result.hexdigest() def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('output', type=Path) parser.add_argument('--driver-dir', type=Path, required=True) parser.add_argument('--samples-dir', type=Path, help='reuse existing av1-*.ivf and reference-* files') parser.add_argument('--gstreamer', action='store_true') parser.add_argument('--extended', action='store_true', help='also test individual coding tools') args = parser.parse_args() work = args.output.resolve() work.mkdir(parents=True, exist_ok=True) samples = args.samples_dir.resolve() if args.samples_dir else work env = os.environ.copy() for key in list(env): if key == 'LD_PRELOAD' or key.startswith('V4L2_TEST_'): del env[key] env.update(LIBVA_DRIVER_NAME='v4l2', LIBVA_DRIVERS_PATH=str(args.driver_dir.resolve()), IRIS_VAAPI_DEBUG='1', GST_VA_ALL_DRIVERS='1', GST_DEBUG='2', GST_DEBUG_NO_COLOR='1', GST_REGISTRY=str(work / 'registry.bin')) def run(command, log, timeout=120): with log.open('w') as file: result = subprocess.run(command, stdout=file, stderr=subprocess.STDOUT, env=env, timeout=timeout) if result.returncode: raise RuntimeError(f'{log.name}: exit {result.returncode}\n{log.read_text()[-6000:]}') if args.gstreamer: flags = shlex.split(subprocess.check_output( ['pkg-config', '--cflags', '--libs', 'gstreamer-app-1.0', 'gstreamer-video-1.0'], text=True)) run(shlex.split(os.environ.get('CXX', 'c++')) + ['-O2', '-Wall', '-Wextra', str(Path(__file__).with_name('av1-download.cpp')), '-o', str(work / 'av1-download')] + flags, work / 'build-download.log') results = [] cases = CASES + (EXTENDED_CASES if args.extended else []) for name, width, height, depth, frames, lag in cases: raw, ffraw = ('nv12', 'nv12') if depth == 8 else ('p010', 'p010le') video = samples / f'av1-{name}.ivf' reference = samples / f'reference-{name}.{raw}' if not args.samples_dir: pixel = 'yuv420p' if depth == 8 else 'yuv420p10le' pattern = f'testsrc2=size={width}x{height}:rate=30,format={pixel}' if name == 'motion8': pattern = "testsrc2=size=1408x800:rate=30,crop=1280:720:x='mod(n*4,128)':y=0" source = ['ffmpeg', '-hide_banner', '-v', 'error', '-f', 'lavfi', '-i', pattern, '-frames:v', str(frames)] if name == 'superres8': # Super-resolution config fields are not exposed by FFmpeg's libaom wrapper. y4m = work / 'superres8.y4m' run(source + ['-f', 'yuv4mpegpipe', '-y', str(y4m)], work / 'generate-y4m.log') run(['aomenc', '--cpu-used=8', '--end-usage=q', '--cq-level=30', '--threads=4', '--row-mt=1', '--passes=1', '--lag-in-frames=0', '--superres-mode=1', '--superres-denominator=12', '--superres-kf-denominator=12', '--ivf', '-o', str(video), str(y4m)], work / f'generate-{name}.log') y4m.unlink() else: usage = 'good' if lag or name in ENCODER_OPTIONS else 'realtime' run(source + ['-c:v', 'libaom-av1', '-usage', usage, '-cpu-used', '8', '-crf', '30', '-b:v', '0', '-g', '30', '-lag-in-frames', str(lag), '-row-mt', '1', '-threads', '4'] + ENCODER_OPTIONS.get(name, []) + ['-f', 'ivf', '-y', str(video)], work / f'generate-{name}.log') run(['ffmpeg', '-hide_banner', '-v', 'error', '-c:v', 'libdav1d', '-i', str(video), '-pix_fmt', ffraw, '-f', 'rawvideo', '-y', str(reference)], work / f'reference-{name}.log') expected = digest(reference) for backend in ['ffmpeg'] + (['gstreamer'] if args.gstreamer else []): output = work / f'{backend}-{name}.{raw}' log = work / f'{backend}-{name}.log' env['IRIS_VAAPI_DUMP'] = str(work / f'{backend}-{name}.obu') if backend == 'ffmpeg': command = ['ffmpeg', '-hide_banner', '-v', 'verbose', '-hwaccel', 'vaapi', '-hwaccel_device', '/dev/dri/renderD128', '-hwaccel_output_format', 'vaapi', '-c:v', 'av1', '-i', str(video), '-vf', f'hwdownload,format={ffraw}', '-fps_mode', 'passthrough', '-f', 'rawvideo', '-y', str(output)] else: fmt = 'NV12' if depth == 8 else 'P010_10LE' location = json.dumps(str(video)) pipeline = (f'filesrc location={location} ! ivfparse ! av1parse ! vaav1dec ! ' f'video/x-raw,format={fmt} ! appsink name=output sync=false') command = [str(work / 'av1-download'), pipeline, str(output)] run(command, log, timeout=30) actual = digest(output) expected_size = width * height * 3 // 2 * (2 if depth == 10 else 1) * frames if actual != expected or output.stat().st_size != expected_size: raise RuntimeError(f'{backend} {name}: output does not match dav1d') text = log.read_text() if re.search(r'recover|VA status 0x|surface sync timeout', text): raise RuntimeError(f'{backend} {name}: driver reported a decoding failure') row = dict(case=name, backend=backend, frames=frames, pixel_identical=True, sha256=actual, reference_replays=text.count('AV1 install reference'), hidden_pictures=text.count('original-show=0')) results.append(row) (work / 'results.json').write_text(json.dumps(results, indent=2) + '\n') output.unlink() print(json.dumps(row), flush=True) print(f'PASS: {len(results)} AV1 cases, {sum(r["frames"] for r in results)} displayed frames') if __name__ == '__main__': main() qcom-va-driver-1.1.0/tests/decoder/capture-pool.cpp000066400000000000000000000025721526764642600222620ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "../../src/internal.hpp" #include int main() { using irisva::capture_pool_size; constexpr size_t mib = 1024 * 1024; // Small frames preserve renderer hints; large frames are byte-budgeted. assert(capture_pool_size(32, 6, 1920 * 1088 * 3 / 2) == 32); assert(capture_pool_size(52, 6, 3840 * 2176 * 3 / 2) == 52); assert(capture_pool_size(32, 6, 7680 * 4320 * 3 / 2) == 16); assert(capture_pool_size(52, 6, 7680 * 4320 * 3 / 2) == 16); assert(capture_pool_size(32, 6, 7680 * 4320 * 3) == 8); // The negotiated allocation size includes padding, regardless of format. assert(capture_pool_size(32, 6, 48 * mib) == 16); assert(capture_pool_size(32, 6, 48 * mib + 1) == 15); assert(capture_pool_size(4, 6, mib) == 6); assert(capture_pool_size(100, 6, mib) == 64); assert(capture_pool_size(32, 16, 48 * mib) == 16); try { capture_pool_size(32, 17, 48 * mib); assert(false); } catch (const irisva::Error &e) { assert(e.status == VA_STATUS_ERROR_ALLOCATION_FAILED); } for (auto minimum : {0u, 65u}) { try { capture_pool_size(32, minimum, mib); assert(false); } catch (const irisva::Error &) { } } try { capture_pool_size(32, 6, 0); assert(false); } catch (const irisva::Error &) { } } qcom-va-driver-1.1.0/tests/decoder/error-completion.c000066400000000000000000000047651526764642600226160ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Test-only shim: mark selected decoded pictures as recoverable V4L2 errors. // cc -shared -fPIC error-completion.c -ldl -o error-completion.so // IRIS_TEST_ERROR_QUEUE=capture|output|both, IRIS_TEST_ERROR_FIRST=46, // IRIS_TEST_ERROR_COUNT=1. Set IRIS_TEST_ERROR_PAYLOAD=keep for nonempty errors. #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include static atomic_int fail_write; int ioctl(int fd, unsigned long request, ...) { va_list args; va_start(args, request); void *arg = va_arg(args, void *); va_end(args); if (request == DMA_BUF_IOCTL_SYNC && (((struct dma_buf_sync *)arg)->flags & DMA_BUF_SYNC_WRITE) && atomic_exchange(&fail_write, 0)) { errno = EIO; return -1; } int (*real_ioctl)(int, unsigned long, ...) = dlsym(RTLD_NEXT, "ioctl"); int result = real_ioctl(fd, request, arg); const char *queue = getenv("IRIS_TEST_ERROR_QUEUE"); if (result || request != VIDIOC_DQBUF || !queue) return result; struct v4l2_buffer *buffer = arg; int capture = buffer->type == V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; int output = buffer->type == V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; if ((!capture && !output) || (strcmp(queue, "both") && strcmp(queue, capture ? "capture" : "output"))) return result; const char *first_env = getenv("IRIS_TEST_ERROR_FIRST"); const char *count_env = getenv("IRIS_TEST_ERROR_COUNT"); uint64_t first = first_env ? strtoull(first_env, NULL, 10) : 46; uint64_t count = count_env ? strtoull(count_env, NULL, 10) : 1; uint64_t token = (uint64_t)buffer->timestamp.tv_sec * 1000000 + buffer->timestamp.tv_usec; if (token < first || token - first >= count) return result; buffer->flags |= V4L2_BUF_FLAG_ERROR; if (getenv("IRIS_TEST_FAIL_RECOVERY")) atomic_store(&fail_write, 1); const char *payload = getenv("IRIS_TEST_ERROR_PAYLOAD"); if (capture && (!payload || strcmp(payload, "keep"))) { buffer->m.planes[0].bytesused = 0; buffer->m.planes[0].data_offset = 0; } fprintf(stderr, "injected %s error token=%llu\n", capture ? "CAPTURE" : "OUTPUT", (unsigned long long)token); if (getenv("IRIS_TEST_ZERO_TIMESTAMP")) buffer->timestamp = (struct timeval){0}; return result; } qcom-va-driver-1.1.0/tests/decoder/error-recovery.py000066400000000000000000000160541526764642600225030ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT """Board regression: error completion, replacement pixels, and stable exports. Usage: error-recovery.py tmp/error-recovery --driver-dir build Requires FFmpeg with libx265, cc/c++, and the timestamp-preserving Iris kernel. Builds the adjacent preload shims; all generated data stays in the output dir. """ import argparse import hashlib import os from pathlib import Path import re import subprocess def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('directory', type=Path) parser.add_argument('--driver-dir', type=Path, required=True) args = parser.parse_args() root = args.directory.resolve() root.mkdir(parents=True, exist_ok=True) source = Path(__file__).resolve().parent error_shim = root / 'error-completion.so' storage_shim = root / 'surface-storage.so' subprocess.run(['cc', '-Wall', '-Wextra', '-shared', '-fPIC', str(source / 'error-completion.c'), '-ldl', '-o', str(error_shim)], check=True) subprocess.run(['c++', '-Wall', '-Wextra', '-shared', '-fPIC', str(source / 'surface-storage.cpp'), '-ldl', '-pthread', '-o', str(storage_shim)], check=True) env = dict(os.environ, LIBVA_DRIVER_NAME='v4l2', LIBVA_DRIVERS_PATH=str(args.driver_dir.resolve()), IRIS_VAAPI_DEBUG='1') for key in list(env): if key.startswith('IRIS_TEST_') or key == 'LD_PRELOAD': del env[key] width, height, frames = 320, 240, 120 frame_samples = width * height * 3 // 2 def decode(name, sample, depth, queue='', first=46, count=1, storage='', drm=False, payload='', failure=''): runenv = env.copy() libraries = [] if queue: libraries.append(str(error_shim)) runenv.update(IRIS_TEST_ERROR_QUEUE=queue, IRIS_TEST_ERROR_FIRST=str(first), IRIS_TEST_ERROR_COUNT=str(count), IRIS_TEST_ERROR_PAYLOAD=payload) if storage: libraries.append(str(storage_shim)) runenv['IRIS_TEST_STORAGE'] = storage if libraries: runenv['LD_PRELOAD'] = ':'.join(libraries) if failure: runenv[failure] = '1' pixel_format = 'nv12' if depth == 8 else 'p010le' filters = ('hwmap=derive_device=drm:mode=read,format=drm_prime,hwmap=mode=read' if drm else 'hwdownload') command = ['ffmpeg', '-hide_banner', '-nostdin', '-nostats', '-init_hw_device', 'drm=drm:/dev/dri/renderD128', '-init_hw_device', 'vaapi=va@drm', '-hwaccel', 'vaapi', '-hwaccel_device', 'va', '-hwaccel_output_format', 'vaapi', '-i', str(sample), '-vf', f'{filters},format={pixel_format}', '-fps_mode', 'passthrough', '-f', 'rawvideo', '-'] result = subprocess.run(command, env=runenv, capture_output=True, timeout=30) log = result.stderr.decode(errors='replace') (root / f'{name}.log').write_text(log) if failure: assert result.returncode != 0 and 'injected CAPTURE error' in log, name assert 'replaced damaged picture' not in log, name if failure == 'IRIS_TEST_FAIL_RECOVERY': assert 'timeout' not in log and 'completed damaged token=46 status=0x1' in log else: assert 'surface sync timeout' in log, name print(f'PASS {name}: failure reported without false recovery', flush=True) return [], log assert result.returncode == 0, f'{name}: {log[-3000:]}' assert 'timeout' not in log, name size = frame_samples * (1 if depth == 8 else 2) assert len(result.stdout) == size * frames, (name, len(result.stdout), size * frames) if storage: assert 'verified persistent surface=' in log, name if queue: assert 'injected ' in log, name images = [hashlib.sha256(result.stdout[i:i + size]).hexdigest() for i in range(0, len(result.stdout), size)] return images, log for depth, bframes in [(8, 0), (10, 0), (8, 3)]: label = f'{depth}bit-b{bframes}' sample = root / f'{label}.hevc' generate = subprocess.run([ 'ffmpeg', '-hide_banner', '-nostdin', '-y', '-f', 'lavfi', '-i', f'testsrc2=size={width}x{height}:rate=30', '-frames:v', str(frames), '-pix_fmt', 'yuv420p' if depth == 8 else 'yuv420p10le', '-c:v', 'libx265', '-x265-params', f'pools=1:frame-threads=1:keyint=30:bframes={bframes}', str(sample)], capture_output=True, timeout=60) (root / f'{label}-generate.log').write_bytes(generate.stderr) assert generate.returncode == 0 reference, _ = decode(label + '-clean', sample, depth) gray = hashlib.sha256((b'\x80' if depth == 8 else b'\x00\x80') * frame_samples).hexdigest() assert gray not in reference cases = [('capture', 46, 1, '', False, ''), ('output', 46, 1, '', False, ''), ('both', 46, 1, '', False, ''), ('capture', 46, 3, '', False, ''), ('capture', 1, 1, '', False, ''), ('capture', 120, 1, '', False, ''), ('capture', 46, 1, '', False, 'keep'), ('capture', 46, 1, '', True, '')] if not bframes: cases += [('capture', 46, 3, storage, False, '') for storage in ['export', 'prime1', 'prime2']] for number, (queue, first, count, storage, drm, payload) in enumerate(cases): name = f'{label}-{number}-{queue}-{storage or "local"}' images, log = decode(name, sample, depth, queue, first, count, storage, drm, payload) changed = [i for i, (a, b) in enumerate(zip(reference, images)) if a != b] assert len(changed) == count and all(images[i] == gray for i in changed), (name, changed) assert log.count('replaced damaged picture') == count, name print(f'PASS {name}: {frames} frames, {count} gray replacements', flush=True) if not bframes: data = sample.read_bytes() starts = list(re.finditer(b'\x00\x00\x00\x01|\x00\x00\x01', data)) nals = [(m.end(), starts[i + 1].start() if i + 1 < len(starts) else len(data)) for i, m in enumerate(starts) if (data[m.end()] >> 1) & 63 < 32] start, end = nals[45] assert end - start > 64 corrupt = root / f'{label}-truncated.hevc' corrupt.write_bytes(data[:start + 64] + data[end:]) images, log = decode(label + '-truncated', corrupt, depth) assert gray in images and 'replaced damaged picture' in log print(f'PASS {label}-truncated: {frames} frames, firmware error recovered', flush=True) if depth == 8 and not bframes: for failure in ['IRIS_TEST_FAIL_RECOVERY', 'IRIS_TEST_ZERO_TIMESTAMP']: decode(label + '-' + failure, sample, depth, queue='capture', failure=failure) if __name__ == '__main__': main() qcom-va-driver-1.1.0/tests/decoder/main10.cpp000066400000000000000000000264321526764642600207360ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Board test: c++ -std=c++17 main10.cpp -lva -lva-drm -o main10 #include #include #include #include #include #include #include #include #include static void require(bool ok, const char *message) { if (!ok) { std::fprintf(stderr, "%s\n", message); std::exit(1); } } static void expect(VAStatus status, VAStatus wanted = VA_STATUS_SUCCESS) { if (status != wanted) { std::fprintf(stderr, "Expected %s, got %s\n", vaErrorStr(wanted), vaErrorStr(status)); std::exit(1); } } static void av1_rejected_pictures(VADisplay display) { VAConfigID config; VAContextID context; VASurfaceID surfaces[2]; expect(vaCreateConfig(display, VAProfileAV1Profile0, VAEntrypointVLD, nullptr, 0, &config)); expect(vaCreateSurfaces(display, VA_RT_FORMAT_YUV420, 128, 128, surfaces, 2, nullptr, 0)); expect(vaCreateContext(display, config, 128, 128, VA_PROGRESSIVE, nullptr, 0, &context)); VADecPictureParameterBufferAV1 base{}; base.current_frame = surfaces[0]; base.frame_width_minus1 = base.frame_height_minus1 = 127; base.seq_info_fields.fields.subsampling_x = base.seq_info_fields.fields.subsampling_y = 1; base.pic_info_fields.bits.show_frame = 1; base.pic_info_fields.bits.uniform_tile_spacing_flag = 1; base.primary_ref_frame = 7; base.base_qindex = 20; base.tile_cols = base.tile_rows = 1; base.matrix_coefficients = 2; std::fill(std::begin(base.ref_frame_map), std::end(base.ref_frame_map), VA_INVALID_SURFACE); // All cases must be rejected before the dummy tile reaches the firmware. auto reject = [&](VADecPictureParameterBufferAV1 picture, VAStatus status, unsigned tile_row = 0) { VASliceParameterBufferAV1 slice{}; slice.slice_data_size = 1; slice.slice_data_flag = VA_SLICE_DATA_FLAG_ALL; slice.tile_row = tile_row; unsigned char byte = 0; VABufferID buffers[3]; expect(vaCreateBuffer(display, context, VAPictureParameterBufferType, sizeof(picture), 1, &picture, &buffers[0])); expect(vaCreateBuffer(display, context, VASliceParameterBufferType, sizeof(slice), 1, &slice, &buffers[1])); expect(vaCreateBuffer(display, context, VASliceDataBufferType, 1, 1, &byte, &buffers[2])); // Cover both parameter-first clients and Chromium's data-first order. unsigned order[] = {0, 1, 2}; do { VABufferID submission[] = {buffers[order[0]], buffers[order[1]], buffers[order[2]]}; expect(vaBeginPicture(display, context, surfaces[0])); expect(vaRenderPicture(display, context, submission, 3)); expect(vaEndPicture(display, context), status); } while (std::next_permutation(std::begin(order), std::end(order))); for (auto buffer : buffers) expect(vaDestroyBuffer(display, buffer)); }; auto picture = base; picture.film_grain_info.film_grain_info_fields.bits.apply_grain = 1; reject(picture, VA_STATUS_ERROR_UNIMPLEMENTED); picture = base; picture.pic_info_fields.bits.large_scale_tile = 1; reject(picture, VA_STATUS_ERROR_UNIMPLEMENTED); picture = base; picture.bit_depth_idx = 1; reject(picture, VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT); picture = base; picture.current_frame = surfaces[1]; reject(picture, VA_STATUS_ERROR_INVALID_SURFACE); picture = base; picture.pic_info_fields.bits.use_superres = 1; picture.superres_scale_denominator = 8; reject(picture, VA_STATUS_ERROR_INVALID_BUFFER); reject(base, VA_STATUS_ERROR_INVALID_BUFFER, 1); picture = base; picture.pic_info_fields.bits.frame_type = 1; reject(picture, VA_STATUS_ERROR_INVALID_BUFFER); expect(vaDestroyContext(display, context)); expect(vaDestroySurfaces(display, surfaces, 2)); expect(vaDestroyConfig(display, config)); } static void av1_slice_buffer_pairing(VADisplay display) { VAConfigID config; VAContextID context; VASurfaceID surface; expect(vaCreateConfig(display, VAProfileAV1Profile0, VAEntrypointVLD, nullptr, 0, &config)); expect(vaCreateSurfaces(display, VA_RT_FORMAT_YUV420, 128, 128, &surface, 1, nullptr, 0)); expect(vaCreateContext(display, config, 128, 128, VA_PROGRESSIVE, nullptr, 0, &context)); VASliceParameterBufferAV1 slice{}; slice.slice_data_size = 1; slice.slice_data_flag = VA_SLICE_DATA_FLAG_ALL; unsigned char bytes[2]{}; VABufferID buffers[4]; // The second offset is outside the first buffer. Moving all parameters // ahead of all data would pair it with the wrong buffer and fail. for (unsigned i = 0; i < 2; ++i) { slice.slice_data_offset = i; expect(vaCreateBuffer(display, context, VASliceParameterBufferType, sizeof(slice), 1, &slice, &buffers[2 * i])); expect(vaCreateBuffer(display, context, VASliceDataBufferType, i + 1, 1, bytes, &buffers[2 * i + 1])); } for (unsigned batch_size : {1u, 3u, 4u}) { expect(vaBeginPicture(display, context, surface)); for (unsigned i = 0; i < 4; i += batch_size) expect(vaRenderPicture(display, context, buffers + i, std::min(batch_size, 4 - i))); // No picture parameters: reject before sending dummy data to firmware. expect(vaEndPicture(display, context), VA_STATUS_ERROR_INVALID_BUFFER); } for (auto buffer : buffers) expect(vaDestroyBuffer(display, buffer)); expect(vaDestroyContext(display, context)); expect(vaDestroySurfaces(display, &surface, 1)); expect(vaDestroyConfig(display, config)); } int main() { int fd = open("/dev/dri/renderD128", O_RDWR); require(fd >= 0, "open DRM device"); auto display = vaGetDisplayDRM(fd); int major, minor; expect(vaInitialize(display, &major, &minor)); std::vector advertised(vaMaxNumProfiles(display)); int profile_count = 0; expect(vaQueryConfigProfiles(display, advertised.data(), &profile_count)); advertised.resize(profile_count); bool av1 = std::find(advertised.begin(), advertised.end(), VAProfileAV1Profile0) != advertised.end(); std::vector mixed_profiles{VAProfileHEVCMain10}; if (av1) mixed_profiles.push_back(VAProfileAV1Profile0); for (VAProfile requested_profile : mixed_profiles) { constexpr unsigned formats = VA_RT_FORMAT_YUV420 | VA_RT_FORMAT_YUV420_10; VAConfigAttrib attr{VAConfigAttribRTFormat, 0}; expect(vaGetConfigAttributes(display, requested_profile, VAEntrypointVLD, &attr, 1)); require(attr.value == formats, "profile must advertise both 8-bit and 10-bit"); VAConfigID default_config; expect(vaCreateConfig(display, requested_profile, VAEntrypointVLD, nullptr, 0, &default_config)); VAProfile profile; VAEntrypoint entrypoint; std::vector config_attrs(vaMaxNumConfigAttributes(display)); int config_count = 0; expect(vaQueryConfigAttributes(display, default_config, &profile, &entrypoint, config_attrs.data(), &config_count)); require(profile == requested_profile && entrypoint == VAEntrypointVLD && config_count == 1 && config_attrs[0].type == VAConfigAttribRTFormat && config_attrs[0].value == formats, "default mixed-depth config formats"); expect(vaDestroyConfig(display, default_config)); const unsigned requests[] = {VA_RT_FORMAT_YUV420, VA_RT_FORMAT_YUV420_10, formats, 0, VA_RT_FORMAT_YUV422, formats | VA_RT_FORMAT_YUV422}; for (unsigned requested : requests) { attr.value = requested; VAConfigID config; bool valid = requested && !(requested & ~formats); expect(vaCreateConfig(display, requested_profile, VAEntrypointVLD, &attr, 1, &config), valid ? VA_STATUS_SUCCESS : VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT); if (!valid) continue; unsigned n = 0; expect(vaQuerySurfaceAttributes(display, config, nullptr, &n)); std::vector attrs(n); unsigned too_small = n - 1; expect(vaQuerySurfaceAttributes(display, config, attrs.data(), &too_small), VA_STATUS_ERROR_MAX_NUM_EXCEEDED); require(too_small == n, "surface attribute count"); expect(vaQuerySurfaceAttributes(display, config, attrs.data(), &n)); bool nv12 = false, p010 = false; for (const auto &a : attrs) { if (a.type == VASurfaceAttribPixelFormat) { nv12 |= a.value.value.i == VA_FOURCC_NV12; p010 |= a.value.value.i == VA_FOURCC_P010; } } require(nv12 && p010, "mixed-depth surface formats"); for (unsigned format : {VA_RT_FORMAT_YUV420, VA_RT_FORMAT_YUV420_10}) { VASurfaceID surface, other; expect(vaCreateSurfaces(display, format, 128, 128, &surface, 1, nullptr, 0)); expect( vaCreateSurfaces(display, format ^ formats, 128, 128, &other, 1, nullptr, 0)); VAContextID context; // Chromium can create the context before allocating its surfaces. expect(vaCreateContext(display, config, 128, 128, VA_PROGRESSIVE, nullptr, 0, &context)); expect(vaBeginPicture(display, context, surface)); expect(vaEndPicture(display, context), VA_STATUS_ERROR_INVALID_BUFFER); expect(vaBeginPicture(display, context, other), VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT); expect(vaDestroyContext(display, context)); expect(vaDestroySurfaces(display, &surface, 1)); expect(vaDestroySurfaces(display, &other, 1)); } expect(vaDestroyConfig(display, config)); } } VAConfigAttrib features{VAConfigAttribDecAV1Features, 0}; expect(vaGetConfigAttributes(display, VAProfileAV1Profile0, VAEntrypointVLD, &features, 1), av1 ? VA_STATUS_SUCCESS : VA_STATUS_ERROR_UNSUPPORTED_PROFILE); if (av1) { require(features.value == 0, "AV1 large scale tiles must not be advertised"); features.value = 1; VAConfigID config; expect( vaCreateConfig(display, VAProfileAV1Profile0, VAEntrypointVLD, &features, 1, &config), VA_STATUS_ERROR_ATTR_NOT_SUPPORTED); expect(vaCreateConfig(display, VAProfileAV1Profile0, VAEntrypointEncSlice, nullptr, 0, &config), VA_STATUS_ERROR_UNSUPPORTED_ENTRYPOINT); av1_rejected_pictures(display); av1_slice_buffer_pairing(display); } VAConfigAttrib attr{VAConfigAttribRTFormat, 0}; expect(vaGetConfigAttributes(display, VAProfileVP9Profile2, VAEntrypointVLD, &attr, 1)); require(attr.value == VA_RT_FORMAT_YUV420_10, "VP9 profile 2 remains 10-bit"); expect(vaTerminate(display)); close(fd); std::puts("Main10/AV1 capability/config/surface tests passed"); } qcom-va-driver-1.1.0/tests/decoder/prime-import.cpp000066400000000000000000000176161526764642600223010ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Board test: c++ -std=c++17 prime-import.cpp -lva -lva-drm -o prime-import #include #include #include #include #include #include #include #include #include #include #include #include static void require(bool ok, const char *message) { if (!ok) { std::fprintf(stderr, "FAIL: %s\n", message); std::exit(1); } } static void expect(VAStatus got, VAStatus wanted = VA_STATUS_SUCCESS) { if (got != wanted) { std::fprintf(stderr, "Expected %s, got %s\n", vaErrorStr(wanted), vaErrorStr(got)); std::exit(1); } } static VAStatus import_surface(VADisplay display, unsigned format, unsigned memory_type, void *descriptor, VASurfaceID *surface) { VASurfaceAttrib attrs[2]{}; // Descriptor first also checks that parsing does not depend on attribute order. attrs[0].type = VASurfaceAttribExternalBufferDescriptor; attrs[0].flags = VA_SURFACE_ATTRIB_SETTABLE; attrs[0].value.type = VAGenericValueTypePointer; attrs[0].value.value.p = descriptor; attrs[1].type = VASurfaceAttribMemoryType; attrs[1].flags = VA_SURFACE_ATTRIB_SETTABLE; attrs[1].value.type = VAGenericValueTypeInteger; attrs[1].value.value.i = memory_type; return vaCreateSurfaces(display, format, 128, 128, surface, 1, attrs, 2); } static void test_format(VADisplay display, unsigned format) { bool p010 = format == VA_RT_FORMAT_YUV420_10; unsigned bytes = p010 ? 2 : 1; VASurfaceAttribExternalBuffers desc{}; desc.pixel_format = p010 ? VA_FOURCC_P010 : VA_FOURCC_NV12; desc.width = desc.height = 128; desc.num_planes = 2; desc.pitches[0] = desc.pitches[1] = 256 * bytes; // Exercise padded rows, padded storage height, and a nonzero base offset. desc.offsets[0] = 128; desc.offsets[1] = desc.offsets[0] + desc.pitches[0] * 160; desc.data_size = (desc.offsets[1] + desc.pitches[1] * 64 + 4095) & ~4095u; int heap = open("/dev/dma_heap/system", O_RDONLY | O_CLOEXEC); require(heap >= 0, "open DMA heap"); dma_heap_allocation_data allocation{}; allocation.len = desc.data_size; allocation.fd_flags = O_RDWR | O_CLOEXEC; require(ioctl(heap, DMA_HEAP_IOCTL_ALLOC, &allocation) == 0, "allocate DMA-BUF"); close(heap); uintptr_t handle = allocation.fd; desc.buffers = &handle; desc.num_buffers = 1; VASurfaceID surface; auto rejected = [&](VASurfaceAttribExternalBuffers bad) { expect( import_surface(display, format, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME, &bad, &surface), VA_STATUS_ERROR_INVALID_PARAMETER); }; auto bad = desc; bad.num_planes = 1; rejected(bad); bad = desc; bad.num_buffers = 2; rejected(bad); bad = desc; bad.buffers = nullptr; rejected(bad); bad = desc; uintptr_t invalid_fd = UINTPTR_MAX; bad.buffers = &invalid_fd; rejected(bad); const unsigned unsupported_flags[] = {VA_SURFACE_EXTBUF_DESC_ENABLE_TILING, VA_SURFACE_EXTBUF_DESC_PROTECTED}; for (unsigned flags : unsupported_flags) { bad = desc; bad.flags = flags; rejected(bad); } bad = desc; bad.width = 127; rejected(bad); bad = desc; bad.pixel_format = p010 ? VA_FOURCC_NV12 : VA_FOURCC_P010; rejected(bad); bad = desc; bad.pitches[0] = bad.pitches[1] = 0; rejected(bad); bad = desc; bad.pitches[1] += 16; rejected(bad); bad = desc; bad.offsets[1] = desc.offsets[0] + desc.pitches[0] * 127; rejected(bad); bad = desc; bad.offsets[1] = UINT_MAX; rejected(bad); bad = desc; bad.data_size = desc.offsets[1]; rejected(bad); bad = desc; bad.data_size += 4096; // Passes plane checks, but exceeds the actual DMA-BUF. rejected(bad); if (p010) { bad = desc; ++bad.offsets[0]; ++bad.offsets[1]; rejected(bad); } expect(import_surface(display, format, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME, &desc, &surface)); for (unsigned flags : {VA_EXPORT_SURFACE_COMPOSED_LAYERS, VA_EXPORT_SURFACE_SEPARATE_LAYERS}) { VADRMPRIMESurfaceDescriptor exported{}; expect(vaExportSurfaceHandle(display, surface, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, VA_EXPORT_SURFACE_READ_WRITE | flags, &exported)); require(exported.fourcc == desc.pixel_format && exported.num_objects == 1, "exported format and object count"); struct stat original{}, shared{}; require(!fstat(allocation.fd, &original) && !fstat(exported.objects[0].fd, &shared) && original.st_dev == shared.st_dev && original.st_ino == shared.st_ino, "import/export must share the original DMA-BUF"); VASurfaceID reimported; expect(import_surface(display, format, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, &exported, &reimported)); close(exported.objects[0].fd); VAImage image; expect(vaDeriveImage(display, reimported, &image)); require(image.format.fourcc == desc.pixel_format && image.num_planes == 2 && image.pitches[0] == desc.pitches[0] && image.pitches[1] == desc.pitches[1] && image.offsets[0] == desc.offsets[0] && image.offsets[1] == desc.offsets[1], "imported image preserves the plane layout"); void *data; expect(vaMapBuffer(display, image.buf, &data)); auto *pixels = static_cast(data); pixels[image.offsets[0]] = 42; pixels[image.offsets[1]] = 73; expect(vaUnmapBuffer(display, image.buf)); expect(vaDestroyImage(display, image.image_id)); expect(vaDestroySurfaces(display, &reimported, 1)); } require(fcntl(allocation.fd, F_GETFD) >= 0, "driver must not close caller's FD"); close(allocation.fd); // The imported surface retains its own FD and the same pixel storage. VAImage image; expect(vaDeriveImage(display, surface, &image)); void *data; expect(vaMapBuffer(display, image.buf, &data)); auto *pixels = static_cast(data); require(pixels[image.offsets[0]] == 42 && pixels[image.offsets[1]] == 73, "shared pixels survive closing the caller's FD"); expect(vaUnmapBuffer(display, image.buf)); expect(vaDestroyImage(display, image.image_id)); expect(vaDestroySurfaces(display, &surface, 1)); } int main() { int fd = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC); require(fd >= 0, "open DRM device"); auto display = vaGetDisplayDRM(fd); int major, minor; expect(vaInitialize(display, &major, &minor)); for (VAProfile profile : {VAProfileHEVCMain, VAProfileHEVCMain10}) { VAConfigID config; expect(vaCreateConfig(display, profile, VAEntrypointVLD, nullptr, 0, &config)); unsigned n = 0; expect(vaQuerySurfaceAttributes(display, config, nullptr, &n)); std::vector attrs(n); expect(vaQuerySurfaceAttributes(display, config, attrs.data(), &n)); bool found = false; for (const auto &a : attrs) { if (a.type == VASurfaceAttribMemoryType) { unsigned types = VA_SURFACE_ATTRIB_MEM_TYPE_VA | VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME | VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2; found = (a.value.value.i & types) == types; } } require(found, "advertise native, PRIME1 and PRIME2 surfaces"); expect(vaDestroyConfig(display, config)); } test_format(display, VA_RT_FORMAT_YUV420); test_format(display, VA_RT_FORMAT_YUV420_10); expect(vaTerminate(display)); close(fd); std::puts("PRIME1/PRIME2 NV12/P010 import tests passed"); } qcom-va-driver-1.1.0/tests/decoder/surface-storage.cpp000066400000000000000000000135111526764642600227350ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Test-only shim: decode into pre-exported or PRIME-imported storage. // c++ -shared -fPIC surface-storage.cpp -ldl -pthread -o surface-storage.so // Set IRIS_TEST_STORAGE=export|prime1|prime2 alongside error-completion.so. #include #include #include #include #include #include #include #include #include #include namespace { struct Identity { dev_t device; ino_t inode; }; std::mutex mutex; std::map storage; auto create_surfaces = reinterpret_cast(dlsym(RTLD_NEXT, "vaCreateSurfaces")); auto destroy_surfaces = reinterpret_cast(dlsym(RTLD_NEXT, "vaDestroySurfaces")); auto export_surface = reinterpret_cast(dlsym(RTLD_NEXT, "vaExportSurfaceHandle")); VAStatus describe(VADisplay display, VASurfaceID surface, VADRMPRIMESurfaceDescriptor &desc) { return export_surface(display, surface, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_COMPOSED_LAYERS, &desc); } void close_objects(const VADRMPRIMESurfaceDescriptor &desc) { for (unsigned i = 0; i < desc.num_objects; ++i) close(desc.objects[i].fd); } } // namespace VAStatus vaCreateSurfaces(VADisplay display, unsigned format, unsigned width, unsigned height, VASurfaceID *surfaces, unsigned count, VASurfaceAttrib *attrs, unsigned attr_count) { VAStatus result = create_surfaces(display, format, width, height, surfaces, count, attrs, attr_count); const char *mode = std::getenv("IRIS_TEST_STORAGE"); if (result != VA_STATUS_SUCCESS || !mode) return result; for (unsigned i = 0; i < count; ++i) { VADRMPRIMESurfaceDescriptor desc{}; result = describe(display, surfaces[i], desc); if (result != VA_STATUS_SUCCESS) return result; if (desc.num_objects != 1 || desc.num_layers != 1) { close_objects(desc); return VA_STATUS_ERROR_OPERATION_FAILED; } struct stat info{}; if (fstat(desc.objects[0].fd, &info)) { close_objects(desc); return VA_STATUS_ERROR_OPERATION_FAILED; } if (std::strcmp(mode, "export")) { // Use the allocation's padding to exercise a nonzero plane base. unsigned end = desc.layers[0].offset[1] + desc.layers[0].pitch[1] * ((height + 1) / 2); if (desc.objects[0].size >= end + 128) { for (unsigned p = 0; p < desc.layers[0].num_planes; ++p) desc.layers[0].offset[p] += 128; } VASurfaceAttribExternalBuffers legacy{}; uintptr_t fd = desc.objects[0].fd; legacy.pixel_format = desc.fourcc; legacy.width = width; legacy.height = height; legacy.data_size = desc.objects[0].size; legacy.num_planes = desc.layers[0].num_planes; for (unsigned p = 0; p < legacy.num_planes; ++p) { legacy.pitches[p] = desc.layers[0].pitch[p]; legacy.offsets[p] = desc.layers[0].offset[p]; } legacy.buffers = &fd; legacy.num_buffers = 1; bool prime1 = !std::strcmp(mode, "prime1"); VASurfaceAttrib imported[2]{}; imported[0].type = VASurfaceAttribMemoryType; imported[0].flags = VA_SURFACE_ATTRIB_SETTABLE; imported[0].value.type = VAGenericValueTypeInteger; imported[0].value.value.i = prime1 ? VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME : VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2; imported[1].type = VASurfaceAttribExternalBufferDescriptor; imported[1].flags = VA_SURFACE_ATTRIB_SETTABLE; imported[1].value.type = VAGenericValueTypePointer; imported[1].value.value.p = prime1 ? static_cast(&legacy) : &desc; result = destroy_surfaces(display, &surfaces[i], 1); if (result == VA_STATUS_SUCCESS) result = create_surfaces(display, format, width, height, &surfaces[i], 1, imported, 2); } close_objects(desc); if (result != VA_STATUS_SUCCESS) return result; std::lock_guard lock(mutex); storage[surfaces[i]] = {info.st_dev, info.st_ino}; } return result; } VAStatus vaSyncSurface(VADisplay display, VASurfaceID surface) { auto sync = reinterpret_cast(dlsym(RTLD_NEXT, "vaSyncSurface")); VAStatus result = sync(display, surface); if (result != VA_STATUS_SUCCESS) return result; std::lock_guard lock(mutex); auto it = storage.find(surface); if (it == storage.end()) return result; VADRMPRIMESurfaceDescriptor desc{}; result = describe(display, surface, desc); if (result != VA_STATUS_SUCCESS) return result; struct stat info{}; bool same = desc.num_objects == 1 && !fstat(desc.objects[0].fd, &info) && info.st_dev == it->second.device && info.st_ino == it->second.inode; close_objects(desc); if (!same) { std::fprintf(stderr, "FAIL: decoded surface changed its DMA-BUF\n"); return VA_STATUS_ERROR_OPERATION_FAILED; } std::fprintf(stderr, "verified persistent surface=%u\n", surface); return result; } VAStatus vaDestroySurfaces(VADisplay display, VASurfaceID *surfaces, int count) { VAStatus result = destroy_surfaces(display, surfaces, count); if (result == VA_STATUS_SUCCESS) { std::lock_guard lock(mutex); for (int i = 0; i < count; ++i) storage.erase(surfaces[i]); } return result; } qcom-va-driver-1.1.0/tests/encoder/000077500000000000000000000000001526764642600171505ustar00rootroot00000000000000qcom-va-driver-1.1.0/tests/encoder/api-smoke.cpp000066400000000000000000000217241526764642600215470ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Board test: c++ -std=c++17 tests/encoder/api-smoke.cpp -lva -lva-drm -o api-smoke #include #include #include #include #include #include #include #include #include #include static void expect(VAStatus actual, VAStatus wanted = VA_STATUS_SUCCESS) { if (actual != wanted) { std::fprintf(stderr, "Expected %s, got %s\n", vaErrorStr(wanted), vaErrorStr(actual)); std::exit(1); } } static unsigned fd_count() { auto dir = opendir("/proc/self/fd"); if (!dir) std::exit(1); unsigned count = 0; while (readdir(dir)) ++count; closedir(dir); return count; } int main() { int fd = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC); if (fd < 0) return 1; VADisplay display = vaGetDisplayDRM(fd); int major, minor; expect(vaInitialize(display, &major, &minor)); VAConfigID config; VAConfigAttrib unsupported{VAConfigAttribEncPackedHeaders, VA_ENC_PACKED_HEADER_SEQUENCE}; expect( vaCreateConfig(display, VAProfileH264High, VAEntrypointEncSlice, &unsupported, 1, &config), VA_STATUS_ERROR_ATTR_NOT_SUPPORTED); VAConfigAttrib rc{VAConfigAttribRateControl, VA_RC_CQP}; expect(vaCreateConfig(display, VAProfileH264High, VAEntrypointEncSlice, &rc, 1, &config)); unsigned initial_fds = 0; std::vector reference; for (unsigned iteration = 0; iteration < 20; ++iteration) { VASurfaceID surfaces[3]; // Alternate direct-compatible storage with a larger pitch/height. The // latter must stage the top-left 128x128 region without changing pixels. unsigned storage_width = iteration % 2 ? 256 : 128; unsigned storage_height = iteration % 2 ? 160 : 128; expect(vaCreateSurfaces(display, VA_RT_FORMAT_YUV420, storage_width, storage_height, surfaces, 3, nullptr, 0)); VAContextID context; expect(vaCreateContext(display, config, 128, 128, VA_PROGRESSIVE, surfaces, 3, &context)); VAImageFormat format{}; format.fourcc = iteration % 2 ? VA_FOURCC_I420 : VA_FOURCC_NV12; format.byte_order = VA_LSB_FIRST; format.bits_per_pixel = 12; VAImage image; expect(vaCreateImage(display, &format, 128, 128, &image)); void *pointer; expect(vaMapBuffer(display, image.buf, &pointer)); std::memset(pointer, 128, image.data_size); for (unsigned y = 0; y < 128; ++y) std::memset(static_cast(pointer) + image.offsets[0] + y * image.pitches[0], 32 + y, 128); expect(vaUnmapBuffer(display, image.buf)); expect(vaPutImage(display, surfaces[0], image.image_id, 0, 0, 128, 128, 0, 0, 128, 128)); expect(vaDestroyImage(display, image.image_id)); expect(vaDeriveImage(display, surfaces[0], &image)); expect(vaMapBuffer(display, image.buf, &pointer)); for (unsigned y = 0; y < 128; ++y) for (unsigned x = 0; x < 128; ++x) if (static_cast( pointer)[image.offsets[0] + y * image.pitches[0] + x] != 32 + y) return 2; expect(vaUnmapBuffer(display, image.buf)); expect(vaDestroyImage(display, image.image_id)); VABufferID coded; expect(vaCreateBuffer(display, context, VAEncCodedBufferType, 1024 * 1024, 1, nullptr, &coded)); expect(vaMapBuffer(display, coded, &pointer), VA_STATUS_ERROR_OPERATION_FAILED); VAEncSequenceParameterBufferH264 sequence{}; sequence.level_idc = 31; sequence.picture_width_in_mbs = sequence.picture_height_in_mbs = 8; sequence.seq_fields.bits.frame_mbs_only_flag = 1; sequence.seq_fields.bits.chroma_format_idc = 1; sequence.ip_period = 1; sequence.intra_period = sequence.intra_idr_period = 30; sequence.max_num_ref_frames = 1; VAEncPictureParameterBufferH264 picture{}; picture.CurrPic.picture_id = surfaces[1]; picture.coded_buf = coded; picture.pic_init_qp = 24; picture.pic_fields.bits.idr_pic_flag = 1; picture.pic_fields.bits.reference_pic_flag = 1; picture.pic_fields.bits.entropy_coding_mode_flag = 1; VAEncSliceParameterBufferH264 slice{}; slice.num_macroblocks = 64; slice.slice_type = 2; slice.macroblock_info = VA_INVALID_ID; auto submit = [&](VAStatus result) { VABufferID params[3]; expect(vaCreateBuffer(display, context, VAEncSequenceParameterBufferType, sizeof(sequence), 1, &sequence, ¶ms[0])); expect(vaCreateBuffer(display, context, VAEncPictureParameterBufferType, sizeof(picture), 1, &picture, ¶ms[1])); expect(vaCreateBuffer(display, context, VAEncSliceParameterBufferType, sizeof(slice), 1, &slice, ¶ms[2])); expect(vaBeginPicture(display, context, surfaces[0])); expect(vaRenderPicture(display, context, params, 3)); expect(vaEndPicture(display, context), result); for (auto id : params) expect(vaDestroyBuffer(display, id)); }; slice.disable_deblocking_filter_idc = 3; submit(VA_STATUS_ERROR_INVALID_PARAMETER); slice.disable_deblocking_filter_idc = 0; slice.slice_alpha_c0_offset_div2 = 7; submit(VA_STATUS_ERROR_INVALID_PARAMETER); slice.slice_alpha_c0_offset_div2 = 0; slice.slice_beta_offset_div2 = -7; submit(VA_STATUS_ERROR_INVALID_PARAMETER); slice.slice_beta_offset_div2 = 0; submit(VA_STATUS_SUCCESS); VAStatus polled = vaSyncBuffer(display, coded, 0); if (polled != VA_STATUS_SUCCESS && polled != VA_STATUS_ERROR_TIMEDOUT) expect(polled); expect(vaSyncBuffer(display, coded, VA_TIMEOUT_INFINITE)); expect(vaMapBuffer(display, coded, &pointer)); auto segment = static_cast(pointer); if (!segment->size || !segment->buf || segment->next || segment->status) return 3; auto *bytes = static_cast(segment->buf); if (reference.empty()) reference.assign(bytes, bytes + segment->size); else if (reference.size() != segment->size || std::memcmp(reference.data(), bytes, segment->size)) { std::fprintf(stderr, "Direct/staged encode mismatch at iteration %u\n", iteration); return 5; } // A mapped bitstream cannot be overwritten by another encode submission. submit(VA_STATUS_ERROR_SURFACE_BUSY); expect(vaUnmapBuffer(display, coded)); picture.pic_fields.bits.idr_pic_flag = 0; slice.slice_type = 1; submit(VA_STATUS_ERROR_UNIMPLEMENTED); // B frames are not silently encoded as P. slice.slice_type = 0; picture.CurrPic.picture_id = surfaces[2]; slice.RefPicList0[0].picture_id = surfaces[2]; submit(VA_STATUS_ERROR_UNIMPLEMENTED); // Unsupported reference selection. slice.RefPicList0[0].picture_id = surfaces[1]; slice.slice_alpha_c0_offset_div2 = slice.slice_beta_offset_div2 = 2; submit(VA_STATUS_ERROR_UNIMPLEMENTED); // Static controls need a new context. slice.slice_alpha_c0_offset_div2 = slice.slice_beta_offset_div2 = 0; submit(VA_STATUS_SUCCESS); // Validation errors did not poison the session. expect(vaSyncSurface(display, surfaces[0])); expect(vaDestroyBuffer(display, coded)); if (iteration == 0) { expect(vaCreateBuffer(display, context, VAEncCodedBufferType, 1, 1, nullptr, &coded)); picture.coded_buf = coded; picture.pic_fields.bits.idr_pic_flag = 1; slice.slice_type = 2; submit(VA_STATUS_SUCCESS); // Hardware completion is asynchronous. expect(vaSyncBuffer(display, coded, VA_TIMEOUT_INFINITE), VA_STATUS_ERROR_NOT_ENOUGH_BUFFER); expect(vaSyncSurface(display, surfaces[0]), VA_STATUS_ERROR_NOT_ENOUGH_BUFFER); expect(vaDestroyBuffer(display, coded)); } expect(vaDestroyContext(display, context)); expect(vaDestroySurfaces(display, surfaces, 3)); unsigned count = fd_count(); if (iteration == 1) initial_fds = count; // Account for lazy EGL/driver initialization. if (iteration > 1 && count != initial_fds) { std::fprintf(stderr, "FD leak: baseline=%u now=%u\n", initial_fds, count); return 4; } } expect(vaDestroyConfig(display, config)); expect(vaTerminate(display)); close(fd); std::puts( "PASS: uploads, deblocking validation, coded mapping/sync, invalid requests, overflow, " "20 context " "lifecycles, fd counts"); } qcom-va-driver-1.1.0/tests/encoder/async.cpp000066400000000000000000000346641526764642600210060ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Board test: c++ -std=c++17 async.cpp -lva -lva-drm -o async // Usage: async OUTPUT_DIRECTORY [--hevc] (directory must already exist) #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool hevc = false; static void expect(VAStatus got, VAStatus wanted = VA_STATUS_SUCCESS) { if (got != wanted) { std::fprintf(stderr, "Expected %s, got %s\n", vaErrorStr(wanted), vaErrorStr(got)); std::exit(1); } } static void require(bool value, const char *message) { if (!value) { std::fprintf(stderr, "%s\n", message); std::exit(1); } } static unsigned fds() { auto *dir = opendir("/proc/self/fd"); require(dir, "opendir"); unsigned n = 0; while (readdir(dir)) ++n; closedir(dir); return n; } struct Session { static constexpr unsigned count = 12, width = 640, height = 480; VADisplay display; VAConfigID config; VAContextID context; VASurfaceID inputs[count], recon[count]; VABufferID coded[count]; explicit Session(VADisplay d, bool overflow = false) : display(d) { VAConfigAttrib attr{VAConfigAttribRateControl, VA_RC_CQP}; expect(vaCreateConfig(d, hevc ? VAProfileHEVCMain : VAProfileH264High, VAEntrypointEncSlice, &attr, 1, &config)); bool staging = std::getenv("IRIS_TEST_STAGING") != nullptr; expect(vaCreateSurfaces(d, VA_RT_FORMAT_YUV420, staging ? 768 : width, staging ? 512 : height, inputs, count, nullptr, 0)); expect(vaCreateSurfaces(d, VA_RT_FORMAT_YUV420, width, height, recon, count, nullptr, 0)); expect(vaCreateContext(d, config, width, height, VA_PROGRESSIVE, inputs, count, &context)); VAImageFormat format{}; format.fourcc = VA_FOURCC_NV12; format.byte_order = VA_LSB_FIRST; format.bits_per_pixel = 12; VAImage image; expect(vaCreateImage(d, &format, width, height, &image)); for (unsigned i = 0; i < count; ++i) { void *data; expect(vaMapBuffer(d, image.buf, &data)); std::memset(data, 128, image.data_size); for (unsigned y = 0; y < height; ++y) for (unsigned x = 0; x < width; ++x) static_cast( data)[image.offsets[0] + y * image.pitches[0] + x] = 16 + ((x / 8 + y / 8 + i * 7) % 200); expect(vaUnmapBuffer(d, image.buf)); expect( vaPutImage(d, inputs[i], image.image_id, 0, 0, width, height, 0, 0, width, height)); expect(vaCreateBuffer(d, context, VAEncCodedBufferType, overflow && !i ? 1 : width * height * 3, 1, nullptr, &coded[i])); } expect(vaDestroyImage(d, image.image_id)); } ~Session() { if (context != VA_INVALID_ID) expect(vaDestroyContext(display, context)); for (auto id : coded) if (id != VA_INVALID_ID) expect(vaDestroyBuffer(display, id)); for (auto id : inputs) if (id != VA_INVALID_SURFACE) expect(vaDestroySurfaces(display, &id, 1)); expect(vaDestroySurfaces(display, recon, count)); expect(vaDestroyConfig(display, config)); } void submit(unsigned frame, VAStatus wanted = VA_STATUS_SUCCESS, unsigned invalid = 0, bool non_idr = false) { unsigned i = frame % count; VAEncSequenceParameterBufferH264 seq{}; seq.level_idc = 31; seq.picture_width_in_mbs = width / 16; seq.picture_height_in_mbs = height / 16; seq.seq_fields.bits.frame_mbs_only_flag = 1; seq.seq_fields.bits.chroma_format_idc = 1; seq.ip_period = 1; seq.intra_period = seq.intra_idr_period = 17; seq.max_num_ref_frames = 1; VAEncPictureParameterBufferH264 pic{}; pic.CurrPic.picture_id = recon[i]; pic.coded_buf = coded[i]; pic.pic_init_qp = (frame / 23) % 2 ? 28 : 24; pic.pic_fields.bits.idr_pic_flag = frame % 17 == 0; pic.pic_fields.bits.reference_pic_flag = 1; pic.pic_fields.bits.entropy_coding_mode_flag = 1; VAEncSliceParameterBufferH264 slice{}; slice.num_macroblocks = width / 16 * (height / 16); slice.slice_type = frame % 17 == 0 ? 2 : 0; slice.macroblock_info = VA_INVALID_ID; slice.RefPicList0[0].picture_id = recon[(frame + count - 1) % count]; if (non_idr) pic.pic_fields.bits.idr_pic_flag = 0; VABufferID parameters[3]; if (hevc) { VAEncSequenceParameterBufferHEVC hs{}; hs.general_profile_idc = 1; hs.general_level_idc = 93; hs.pic_width_in_luma_samples = width; hs.pic_height_in_luma_samples = height; hs.seq_fields.bits.chroma_format_idc = 1; hs.log2_diff_max_min_luma_coding_block_size = 2; hs.ip_period = 1; VAEncPictureParameterBufferHEVC hp{}; hp.decoded_curr_pic.picture_id = recon[i]; hp.coded_buf = coded[i]; hp.pic_init_qp = pic.pic_init_qp; hp.pic_fields.bits.idr_pic_flag = pic.pic_fields.bits.idr_pic_flag; hp.pic_fields.bits.coding_type = slice.slice_type == 2 ? 1 : 2; hp.pic_fields.bits.reference_pic_flag = 1; VAEncSliceParameterBufferHEVC sl{}; sl.num_ctu_in_slice = (width + 31) / 32 * ((height + 31) / 32); sl.slice_type = slice.slice_type == 2 ? 2 : 1; sl.ref_pic_list0[0].picture_id = slice.RefPicList0[0].picture_id; sl.slice_fields.bits.last_slice_of_pic_flag = 1; sl.slice_fields.bits.slice_loop_filter_across_slices_enabled_flag = 1; if (invalid == 1) sl.slice_type = 0; // B frame. if (invalid == 2) sl.num_ctu_in_slice = 1; // Partial picture. if (invalid == 3) hp.pic_fields.bits.tiles_enabled_flag = 1; if (invalid == 4) hp.pic_init_qp = 52; if (invalid == 5) sl.slice_beta_offset_div2 = 7; if (invalid == 6) sl.ref_pic_list0[0].flags = VA_PICTURE_HEVC_LONG_TERM_REFERENCE; expect(vaCreateBuffer(display, context, VAEncSequenceParameterBufferType, sizeof(hs), 1, &hs, ¶meters[0])); expect(vaCreateBuffer(display, context, VAEncPictureParameterBufferType, sizeof(hp), 1, &hp, ¶meters[1])); expect(vaCreateBuffer(display, context, VAEncSliceParameterBufferType, sizeof(sl), 1, &sl, ¶meters[2])); } else { expect(vaCreateBuffer(display, context, VAEncSequenceParameterBufferType, sizeof(seq), 1, &seq, ¶meters[0])); expect(vaCreateBuffer(display, context, VAEncPictureParameterBufferType, sizeof(pic), 1, &pic, ¶meters[1])); expect(vaCreateBuffer(display, context, VAEncSliceParameterBufferType, sizeof(slice), 1, &slice, ¶meters[2])); } expect(vaBeginPicture(display, context, inputs[i])); expect(vaRenderPicture(display, context, parameters, 3)); expect(vaEndPicture(display, context), wanted); for (auto id : parameters) expect(vaDestroyBuffer(display, id)); } void collect(unsigned frame, std::vector &stream) { unsigned i = frame % count; if (frame % 3 == 0) { auto polled = vaSyncBuffer(display, coded[i], 0); require(polled == VA_STATUS_SUCCESS || polled == VA_STATUS_ERROR_TIMEDOUT, "nonblocking sync"); expect(vaSyncBuffer(display, coded[i], VA_TIMEOUT_INFINITE)); } else if (frame % 3 == 1 && inputs[i] != VA_INVALID_SURFACE) { expect(vaSyncSurface(display, inputs[i])); } // Every third frame deliberately maps without an explicit sync. void *data; expect(vaMapBuffer(display, coded[i], &data)); auto *seg = static_cast(data); require(seg->size && seg->buf && !seg->next && !seg->status, "invalid coded segment"); auto *bytes = static_cast(seg->buf); stream.insert(stream.end(), bytes, bytes + seg->size); expect(vaUnmapBuffer(display, coded[i])); VASurfaceStatus status; expect(vaQuerySurfaceStatus(display, recon[i], &status)); require(status == VASurfaceReady, "reconstruction status is not ready after sync"); } }; static std::vector encode(VADisplay d, unsigned depth, bool non_idr = false) { Session session(d); std::vector stream; constexpr unsigned frames = 96; for (unsigned i = 0; i < frames; ++i) { if (i >= depth) session.collect(i - depth, stream); session.submit(i, VA_STATUS_SUCCESS, 0, non_idr && i && i % 17 == 0); } for (unsigned i = frames - depth; i < frames; ++i) session.collect(i, stream); return stream; } int main(int argc, char **argv) { if (argc != 2 && !(argc == 3 && !std::strcmp(argv[2], "--hevc"))) return 2; hevc = argc == 3; int fd = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC); require(fd >= 0, "DRM open"); VADisplay display = vaGetDisplayDRM(fd); int major, minor; expect(vaInitialize(display, &major, &minor)); if (hevc) { VAConfigID unsupported; expect(vaCreateConfig(display, VAProfileHEVCMain10, VAEntrypointEncSlice, nullptr, 0, &unsupported), VA_STATUS_ERROR_UNSUPPORTED_ENTRYPOINT); for (unsigned invalid = 1; invalid <= 6; ++invalid) { Session s(display); if (invalid == 6) { s.submit(0); std::vector output; s.collect(0, output); } s.submit(invalid == 6 ? 1 : 0, invalid == 4 || invalid == 5 ? VA_STATUS_ERROR_INVALID_PARAMETER : VA_STATUS_ERROR_UNIMPLEMENTED, invalid); // Rejection before submission must leave the session usable. s.submit(invalid == 6 ? 1 : 0); } } auto serial = encode(display, 1); auto parallel = encode(display, 8); require(serial == parallel, "async changed QP/IDR/reference behavior"); require( encode(display, 1, true) == serial && encode(display, 8, true) == serial, "non-IDR I requests must match explicitly requested IDRs, including following P frames"); FILE *file = std::fopen((std::string(argv[1]) + (hevc ? "/async.hevc" : "/async.h264")).c_str(), "wb"); require(file && std::fwrite(parallel.data(), 1, parallel.size(), file) == parallel.size(), "write"); require(!std::fclose(file), "close"); // Application think time is not an active hardware wait timeout. { Session s(display); for (unsigned i = 0; i < 8; ++i) s.submit(i); std::this_thread::sleep_for(std::chrono::seconds(6)); std::vector output; for (unsigned i = 0; i < 8; ++i) s.collect(i, output); require(!output.empty(), "lost output while client was idle"); } unsigned baseline = fds(); for (unsigned round = 0; round < 10; ++round) { { Session s(display); for (unsigned i = 0; i < 4; ++i) s.submit(i); for (unsigned i = 0; i < 4; ++i) { expect(vaDestroySurfaces(display, &s.inputs[i], 1)); s.inputs[i] = VA_INVALID_SURFACE; } expect(vaDestroyBuffer(display, s.coded[1])); s.coded[1] = VA_INVALID_ID; // Destruction drains outstanding work, preserving surviving coded buffers. expect(vaDestroyContext(display, s.context)); s.context = VA_INVALID_ID; std::vector output; for (unsigned i : {0u, 2u, 3u}) s.collect(i, output); require(!output.empty(), "lost output at context destruction"); } require(fds() == baseline, "FD leak after destroying queued resources"); } { Session s(display, true); s.submit(0); expect(vaSyncBuffer(display, s.coded[0], VA_TIMEOUT_INFINITE), VA_STATUS_ERROR_NOT_ENOUGH_BUFFER); void *data; expect(vaMapBuffer(display, s.coded[0], &data), VA_STATUS_ERROR_NOT_ENOUGH_BUFFER); s.submit(1, VA_STATUS_ERROR_ENCODING_ERROR); // A distinct, unused input surface. } require(fds() == baseline, "FD leak after asynchronous overflow"); if (std::getenv("IRIS_TEST_DELAY_COMPLETION")) { // Requires delay-completion.so. Keep the hardware running while hiding // DQBUF completions, then make them visible again after a caller timeout. { Session s(display); setenv("IRIS_TEST_BLOCK_DQBUF", "1", 1); s.submit(0); expect(vaSyncBuffer(display, s.coded[0], 0), VA_STATUS_ERROR_TIMEDOUT); expect(vaSyncBuffer(display, s.coded[0], 1000000), VA_STATUS_ERROR_TIMEDOUT); unsetenv("IRIS_TEST_BLOCK_DQBUF"); std::vector output; s.collect(0, output); } { Session s(display); setenv("IRIS_TEST_BLOCK_DQBUF", "1", 1); s.submit(0); expect(vaSyncBuffer(display, s.coded[0], VA_TIMEOUT_INFINITE), VA_STATUS_ERROR_ENCODING_ERROR); unsetenv("IRIS_TEST_BLOCK_DQBUF"); void *data; expect(vaMapBuffer(display, s.coded[0], &data), VA_STATUS_ERROR_ENCODING_ERROR); } require(fds() == baseline, "FD leak after timeout recovery/abort"); std::puts( "PASS: caller timeouts preserve jobs; active hardware wait timeout aborts safely"); } expect(vaTerminate(display)); close(fd); std::puts("PASS: serial/async bitstreams identical, sync/map/status, QP/IDR barriers, pending " "resource destruction, idle client, overflow and FD counts"); } qcom-va-driver-1.1.0/tests/encoder/delay-completion.c000066400000000000000000000013141526764642600225600ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Test-only shim: hide completed V4L2 buffers to exercise VA sync timeouts. // cc -shared -fPIC delay-completion.c -ldl -o delay-completion.so #define _GNU_SOURCE #include #include #include #include #include #include int ioctl(int fd, unsigned long request, ...) { va_list args; va_start(args, request); void *arg = va_arg(args, void *); va_end(args); if (request == VIDIOC_DQBUF && getenv("IRIS_TEST_BLOCK_DQBUF")) { errno = EAGAIN; return -1; } int (*real_ioctl)(int, unsigned long, ...) = dlsym(RTLD_NEXT, "ioctl"); return real_ioctl(fd, request, arg); } qcom-va-driver-1.1.0/tests/encoder/ffmpeg-smoke.py000066400000000000000000000110621526764642600221020ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT """Hardware H.264/HEVC encode smoke test; uses the selected libva driver. Run on the Iris board: python3 tests/encoder/ffmpeg-smoke.py /path/to/results Set LIBVA_DRIVER_NAME and LIBVA_DRIVERS_PATH to test an uninstalled build. Add --hevc to test HEVC Main. Requires FFmpeg VA encoding/software decoding and ffprobe. """ import json import os from pathlib import Path import re import subprocess import sys import time root = Path(sys.argv[1]).resolve() root.mkdir(parents=True, exist_ok=True) summary = [] codec = 'hevc' if '--hevc' in sys.argv[2:] else 'h264' def run(args, name, timeout=90, env=None): start = time.monotonic() p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=timeout, env=env) (root / (name + '.log')).write_text(p.stderr) if p.returncode: raise RuntimeError(f'{name} failed ({p.returncode}); see {root / (name + ".log")}') return p.stdout, time.monotonic() - start cases = [ ('baseline-cqp', 640, 480, 'constrained_baseline', ['-qp', '24'], 90, 'mp4'), ('main-cbr', 640, 480, 'main', ['-rc_mode', 'CBR', '-b:v', '2M'], 90, 'mp4'), ('high-vbr', 640, 480, 'high', ['-rc_mode', 'VBR', '-b:v', '2M', '-maxrate', '3M'], 90, 'mp4'), ('high-cqp', 640, 480, 'high', ['-qp', '24'], 90, 'mp4'), ('crop', 642, 482, 'high', ['-qp', '24'], 60, 'mp4'), ('1080p', 1920, 1080, 'high', ['-qp', '24'], 120, 'mp4'), ('4k', 3840, 2160, 'high', ['-qp', '24'], 60, 'mp4'), ('one-frame', 128, 128, 'high', ['-qp', '24'], 1, 'mp4'), ('non-idr-gops', 640, 480, 'high', ['-rc_mode', 'CBR', '-b:v', '2M', '-idr_interval', '2147483647', '-async_depth', '4'], 90, 'mp4'), ] if codec == 'hevc': cases = [(name.replace('high-', 'main-'), w, h, 'main', rc, frames, container) for name, w, h, profile, rc, frames, container in cases if name != 'baseline-cqp'] for name, w, h, profile, rc, frames, container in cases: raw = root / f'{w}x{h}-{frames}.nv12' if not raw.exists(): run(['ffmpeg', '-nostdin', '-v', 'warning', '-f', 'lavfi', '-i', f'testsrc2=size={w}x{h}:rate=30,format=nv12', '-frames:v', str(frames), '-f', 'rawvideo', '-y', str(raw)], name + '-source') env = dict(os.environ, IRIS_VAAPI_DEBUG='1') output = root / (name + '.' + container) _, elapsed = run(['ffmpeg', '-nostdin', '-v', 'verbose', '-vaapi_device', '/dev/dri/renderD128', '-f', 'rawvideo', '-pixel_format', 'nv12', '-video_size', f'{w}x{h}', '-framerate', '30', '-i', str(raw), '-vf', 'hwupload', '-c:v', codec + '_vaapi', '-profile:v', profile, '-bf', '0', '-g', '30', *rc, '-frames:v', str(frames), '-y', str(output)], name, env=env) info, _ = run(['ffprobe', '-v', 'error', '-select_streams', 'v:0', '-count_frames', '-show_entries', 'stream=codec_name,profile,width,height,nb_read_frames,extradata_size:frame=key_frame,pict_type', '-of', 'json', str(output)], name + '-probe') info = json.loads(info) stream = info['streams'][0] assert (stream['width'], stream['height'], int(stream['nb_read_frames'])) == (w, h, frames), info assert stream['codec_name'] == codec, info assert stream['profile'] == {'constrained_baseline': 'Constrained Baseline', 'main': 'Main', 'high': 'High'}[profile], info assert sum(f['key_frame'] for f in info['frames']) == (frames + 29) // 30, info assert all(f['pict_type'] in ('I', 'P') for f in info['frames']), info run(['ffmpeg', '-nostdin', '-hide_banner', '-xerror', '-i', str(output), '-f', 'rawvideo', '-pixel_format', 'nv12', '-video_size', f'{w}x{h}', '-framerate', '30', '-i', str(raw), '-lavfi', 'ssim', '-frames:v', str(frames), '-f', 'null', '-'], name + '-quality') log = (root / (name + '-quality.log')).read_text() quality = float(re.search(r'All:([0-9.]+)', log)[1]) assert quality > 0.97, (name, quality) paths = re.findall(r'encoder input path=(direct|staging|cpu-mmap)', (root / (name + '.log')).read_text()) assert len(paths) == frames, (name, len(paths), frames) row = dict(name=name, frames=frames, seconds=round(elapsed, 3), fps=round(frames / elapsed, 1), ssim=quality, input_paths={path: paths.count(path) for path in sorted(set(paths))}) summary.append(row) print(json.dumps(row), flush=True) (root / 'summary.json').write_text(json.dumps(summary, indent=2) + '\n') print('PASS: all encoded streams decoded, frame counts/keyframes/profiles/dimensions/quality verified.') qcom-va-driver-1.1.0/tests/encoder/reject-coherent.c000066400000000000000000000014611526764642600223770ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Hardware fallback test only: // cc -shared -fPIC tests/encoder/reject-coherent.c -ldl \ // $(pkg-config --cflags libdrm) -o reject-coherent.so // LD_PRELOAD=$PWD/reject-coherent.so ./api-smoke #define _GNU_SOURCE #include #include #include #include #ifndef MSM_BO_CACHED_COHERENT #define MSM_BO_CACHED_COHERENT 0x080000 #endif int drmIoctl(int fd, unsigned long request, void *arg) { if (request == DRM_IOCTL_MSM_GEM_NEW && (((struct drm_msm_gem_new *)arg)->flags & MSM_BO_CACHED_COHERENT)) { errno = EINVAL; return -1; } int (*next)(int, unsigned long, void *) = dlsym(RTLD_NEXT, "drmIoctl"); if (!next) { errno = ENOSYS; return -1; } return next(fd, request, arg); } qcom-va-driver-1.1.0/tests/encoder/writable-export.cpp000066400000000000000000000271411526764642600230110ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // GPU producer regression matching Sunshine: writable PRIME2 export, R8/GR88 // render targets, glFlush (no glFinish), then H.264/HEVC VAAPI encoding. // c++ -std=c++17 writable-export.cpp -o writable-export $(pkg-config --cflags --libs \ // libavcodec libavutil libva libdrm gbm egl glesv2) // Usage: writable-export OUTPUT_DIRECTORY [WIDTH HEIGHT FRAMES] [--hevc] extern "C" { #include #include #include #include } #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool hevc = false; static void require(bool value, const char *message) { if (!value) { std::fprintf(stderr, "%s\n", message); std::exit(1); } } static void avcheck(int result) { if (result < 0) { char message[AV_ERROR_MAX_STRING_SIZE]; av_strerror(result, message, sizeof(message)); require(false, message); } } static void vacheck(VAStatus status) { require(status == VA_STATUS_SUCCESS, vaErrorStr(status)); } static unsigned sample(unsigned frame, unsigned stripe, unsigned plane, unsigned channel) { return plane ? 80 + (frame * 3 + channel * 31) % 96 : 16 + (frame * 7 + stripe * 19) % 220; } struct Gpu { int fd = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC); gbm_device *gbm = nullptr; EGLDisplay display = EGL_NO_DISPLAY; EGLContext context = EGL_NO_CONTEXT; PFNEGLCREATEIMAGEKHRPROC create_image = nullptr; PFNEGLDESTROYIMAGEKHRPROC destroy_image = nullptr; PFNGLEGLIMAGETARGETTEXTURE2DOESPROC bind_image = nullptr; Gpu() { require(fd >= 0, "open DRM"); gbm = gbm_create_device(fd); require(gbm, "create GBM"); auto platform = reinterpret_cast( eglGetProcAddress("eglGetPlatformDisplayEXT")); create_image = reinterpret_cast(eglGetProcAddress("eglCreateImageKHR")); destroy_image = reinterpret_cast(eglGetProcAddress("eglDestroyImageKHR")); bind_image = reinterpret_cast( eglGetProcAddress("glEGLImageTargetTexture2DOES")); require(platform && create_image && destroy_image && bind_image, "EGL entrypoints"); display = platform(EGL_PLATFORM_GBM_KHR, gbm, nullptr); require(eglInitialize(display, nullptr, nullptr), "initialize EGL"); require(eglBindAPI(EGL_OPENGL_ES_API), "bind GLES"); const EGLint ctx_attrs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE}; context = eglCreateContext(display, EGL_NO_CONFIG_KHR, EGL_NO_CONTEXT, ctx_attrs); require(context != EGL_NO_CONTEXT && eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, context), "EGL context"); } ~Gpu() { eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); eglDestroyContext(display, context); eglTerminate(display); gbm_device_destroy(gbm); close(fd); } void fill(VADisplay va, VASurfaceID surface, unsigned frame) { VADRMPRIMESurfaceDescriptor desc{}, again{}; vacheck(vaExportSurfaceHandle( va, surface, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, VA_EXPORT_SURFACE_WRITE_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS, &desc)); vacheck(vaExportSurfaceHandle( va, surface, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, VA_EXPORT_SURFACE_READ_WRITE | VA_EXPORT_SURFACE_SEPARATE_LAYERS, &again)); require(desc.num_objects == 1 && desc.num_layers == 2 && again.num_objects == 1, "NV12 descriptor"); struct stat first{}, second{}; require(!fstat(desc.objects[0].fd, &first) && !fstat(again.objects[0].fd, &second) && first.st_dev == second.st_dev && first.st_ino == second.st_ino, "export changed storage"); close(again.objects[0].fd); for (unsigned plane = 0; plane < 2; ++plane) { const auto &layer = desc.layers[plane]; const auto &object = desc.objects[layer.object_index[0]]; unsigned w = desc.width / (plane + 1), h = desc.height / (plane + 1); const EGLint attrs[] = {EGL_WIDTH, EGLint(w), EGL_HEIGHT, EGLint(h), EGL_LINUX_DRM_FOURCC_EXT, EGLint(layer.drm_format), EGL_DMA_BUF_PLANE0_FD_EXT, object.fd, EGL_DMA_BUF_PLANE0_OFFSET_EXT, EGLint(layer.offset[0]), EGL_DMA_BUF_PLANE0_PITCH_EXT, EGLint(layer.pitch[0]), EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, EGLint(object.drm_format_modifier), EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, EGLint(object.drm_format_modifier >> 32), EGL_NONE}; auto image = create_image(display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attrs); require(image != EGL_NO_IMAGE_KHR, "import writable plane"); GLuint texture, fbo; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); bind_image(GL_TEXTURE_2D, image); glGenFramebuffers(1, &fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); require(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "plane is not renderable"); glEnable(GL_SCISSOR_TEST); for (unsigned stripe = 0; stripe < 8; ++stripe) { unsigned x = stripe * w / 8, end = (stripe + 1) * w / 8; glScissor(x, 0, end - x, h); glClearColor(sample(frame, stripe, plane, 0) / 255.f, sample(frame, stripe, plane, 1) / 255.f, 0, 1); glClear(GL_COLOR_BUFFER_BIT); } glDisable(GL_SCISSOR_TEST); glBindFramebuffer(GL_FRAMEBUFFER, 0); glDeleteFramebuffers(1, &fbo); glDeleteTextures(1, &texture); require(destroy_image(display, image), "destroy EGL image"); } glFlush(); // The backend must wait for this GPU writer before V4L2 QBUF. require(glGetError() == GL_NO_ERROR, "GPU write failed"); close(desc.objects[0].fd); } }; static std::vector encode(AVBufferRef *device, Gpu *gpu, unsigned w, unsigned h, unsigned count) { AVBufferRef *frames = av_hwframe_ctx_alloc(device); require(frames, "allocate frames context"); auto *hw = reinterpret_cast(frames->data); hw->format = AV_PIX_FMT_VAAPI; hw->sw_format = AV_PIX_FMT_NV12; hw->width = w; hw->height = h; hw->initial_pool_size = 8; avcheck(av_hwframe_ctx_init(frames)); const AVCodec *codec = avcodec_find_encoder_by_name(hevc ? "hevc_vaapi" : "h264_vaapi"); require(codec, "VAAPI encoder"); auto *ctx = avcodec_alloc_context3(codec); require(ctx, "allocate encoder"); ctx->width = w; ctx->height = h; ctx->pix_fmt = AV_PIX_FMT_VAAPI; ctx->time_base = AVRational{1, 60}; ctx->framerate = AVRational{60, 1}; ctx->gop_size = 30; ctx->max_b_frames = 0; ctx->refs = 1; ctx->profile = hevc ? AV_PROFILE_HEVC_MAIN : AV_PROFILE_H264_HIGH; ctx->bit_rate = 20000000; ctx->hw_frames_ctx = av_buffer_ref(frames); avcheck(av_opt_set(ctx->priv_data, "rc_mode", "CBR", 0)); avcheck(av_opt_set_int(ctx->priv_data, "async_depth", 4, 0)); // Sunshine uses multiple GOPs per IDR. Exercise GPU input at CRA/I boundaries. avcheck(av_opt_set_int(ctx->priv_data, "idr_interval", 2147483647, 0)); avcheck(avcodec_open2(ctx, codec, nullptr)); auto *va_device = reinterpret_cast( reinterpret_cast(device->data)->hwctx); auto *packet = av_packet_alloc(); require(packet, "allocate packet"); std::vector bytes; unsigned packets = 0; auto receive = [&] { int result; while ((result = avcodec_receive_packet(ctx, packet)) >= 0) { bytes.insert(bytes.end(), packet->data, packet->data + packet->size); ++packets; av_packet_unref(packet); } require(result == AVERROR(EAGAIN) || result == AVERROR_EOF, "receive packet"); }; for (unsigned i = 0; i < count; ++i) { auto *frame = av_frame_alloc(); require(frame, "allocate frame"); avcheck(av_hwframe_get_buffer(frames, frame, 0)); if (gpu) { gpu->fill(va_device->display, VASurfaceID(uintptr_t(frame->data[3])), i); } else { auto *cpu = av_frame_alloc(); require(cpu, "allocate CPU frame"); cpu->format = AV_PIX_FMT_NV12; cpu->width = w; cpu->height = h; avcheck(av_frame_get_buffer(cpu, 32)); for (unsigned plane = 0; plane < 2; ++plane) for (unsigned y = 0; y < h / (plane + 1); ++y) for (unsigned x = 0; x < w; ++x) cpu->data[plane][y * cpu->linesize[plane] + x] = sample(i, x * 8 / w, plane, plane ? x % 2 : 0); avcheck(av_hwframe_transfer_data(frame, cpu, 0)); av_frame_free(&cpu); } frame->pts = i; avcheck(avcodec_send_frame(ctx, frame)); av_frame_free(&frame); receive(); } avcheck(avcodec_send_frame(ctx, nullptr)); receive(); require(packets == count, "missing output frames"); av_packet_free(&packet); avcodec_free_context(&ctx); av_buffer_unref(&frames); return bytes; } int main(int argc, char **argv) { if (argc > 1 && !std::strcmp(argv[argc - 1], "--hevc")) { hevc = true; --argc; } if (argc != 2 && argc != 5) return 2; unsigned w = argc == 5 ? std::atoi(argv[2]) : 640; unsigned h = argc == 5 ? std::atoi(argv[3]) : 480; unsigned frames = argc == 5 ? std::atoi(argv[4]) : 96; require(w && !(w % 16) && h && !(h % 2) && frames, "invalid dimensions/count"); AVBufferRef *device = nullptr; avcheck( av_hwdevice_ctx_create(&device, AV_HWDEVICE_TYPE_VAAPI, "/dev/dri/renderD128", nullptr, 0)); Gpu gpu; auto cpu = encode(device, nullptr, w, h, frames); auto rendered = encode(device, &gpu, w, h, frames); require(cpu == rendered, "GPU-written and CPU-uploaded bitstreams differ"); auto *file = std::fopen( (std::string(argv[1]) + (hevc ? "/writable.hevc" : "/writable.h264")).c_str(), "wb"); require(file && std::fwrite(rendered.data(), 1, rendered.size(), file) == rendered.size(), "write"); require(!std::fclose(file), "close output"); av_buffer_unref(&device); std::printf("PASS: %u GPU-written frames match CPU upload byte-for-byte; writable exports, " "stable storage, EGL plane rendering, glFlush fences and async encode\n", frames); } qcom-va-driver-1.1.0/tests/vpp/000077500000000000000000000000001526764642600163365ustar00rootroot00000000000000qcom-va-driver-1.1.0/tests/vpp/check-abi.py000066400000000000000000000032471526764642600205240ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT """Optional ABI audit against SDK 6.7; the SDK is not used by the build.""" import argparse from pathlib import Path import subprocess import tempfile p = argparse.ArgumentParser(description=__doc__) p.add_argument('tools', type=Path, help='Open Access Tools directory') p.add_argument('sdk', type=Path, help='HexagonSDK6.7 directory') a = p.parse_args() root = Path(__file__).resolve().parents[2] cc = a.tools.resolve() / 'bin/hexagon-clang' sdk = a.sdk.resolve() with tempfile.TemporaryDirectory() as tmp: source = Path(tmp) / 'audit.c' source.write_text('''#include #include #include #include #include #include "qurt_abi_checks.h" _Static_assert(QURT_THREAD_ATTR_TCB_PARTITION_DEFAULT == 0 && QURT_THREAD_ATTR_STID_DEFAULT == 0 && QURT_THREAD_DEFAULT_GROUP_ID == 0 && QURT_THREAD_ATTR_AUTOSTACK_DEFAULT == 0 && QURT_THREAD_ATTR_BUS_PRIO_DEFAULT == 255 && QURT_THREAD_ATTR_TIMETEST_ID_DEFAULT == -2 && QURT_THREAD_ATTR_CREATE_LEGACY == 0, "thread initialization defaults"); ''') for arch in ('v68', 'v69', 'v73', 'v75', 'v79', 'v81'): subprocess.run([str(cc), '--no-default-config', '-m' + arch, '-fshort-enums', '-fsyntax-only', '-I' + str(sdk / 'incs'), '-I' + str(sdk / 'incs/stddef'), '-I' + str(sdk / ('rtos/qurt/compute' + arch) / 'include/qurt'), '-I' + str(root / 'src/cdsp'), str(source)], check=True) print(f'PASS: {arch} thread, semaphore, power and compute-resource ABI match SDK 6.7', flush=True) qcom-va-driver-1.1.0/tests/vpp/msm-allocation.c000066400000000000000000000053111526764642600214210ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Exercise the cache-mode contract without a DRM device. #include #include #include #include #include #include #ifndef MSM_BO_CACHED_COHERENT #define MSM_BO_CACHED_COHERENT 0x080000 #endif int irisva_msm_allocate(int fd, uint64_t size, int *coherent); int irisva_msm_allocate_coherent(int fd, uint64_t size); static int coherent_error, export_error, wc_requests, closed_handles; int drmIoctl(int fd, unsigned long request, void *arg) { (void)fd; if (request == DRM_IOCTL_MSM_GEM_NEW) { struct drm_msm_gem_new *allocation = arg; assert(allocation->size == 4096); if (allocation->flags == MSM_BO_CACHED_COHERENT && coherent_error) { errno = coherent_error; return -1; } if (allocation->flags == MSM_BO_WC) ++wc_requests; else assert(allocation->flags == MSM_BO_CACHED_COHERENT); allocation->handle = 42; } else { assert(request == DRM_IOCTL_GEM_CLOSE); assert(((struct drm_gem_close *)arg)->handle == 42); ++closed_handles; errno = 0; // Closing a handle must not erase an export failure's errno. } return 0; } int drmPrimeHandleToFD(int fd, uint32_t handle, uint32_t flags, int *prime_fd) { (void)fd; assert(handle == 42); assert(flags == (DRM_CLOEXEC | DRM_RDWR)); if (export_error) { errno = export_error; return -1; } *prime_fd = 123; // No real descriptors are created by these mocks. return 0; } int main(void) { assert(irisva_msm_allocate_coherent(0, 4096) == 123); assert(closed_handles == 1 && wc_requests == 0); const int failures[] = {EINVAL, EOPNOTSUPP, EACCES, ENOMEM}; for (unsigned i = 0; i < sizeof(failures) / sizeof(failures[0]); ++i) { coherent_error = failures[i]; assert(irisva_msm_allocate_coherent(0, 4096) == -1); assert(errno == coherent_error); assert(wc_requests == 0 && closed_handles == 1); } coherent_error = 0; export_error = EMFILE; assert(irisva_msm_allocate_coherent(0, 4096) == -1); assert(errno == EMFILE && closed_handles == 2 && wc_requests == 0); export_error = 0; int coherent = -1; coherent_error = EINVAL; assert(irisva_msm_allocate(0, 4096, &coherent) == 123); assert(!coherent && wc_requests == 1 && closed_handles == 3); coherent_error = EACCES; assert(irisva_msm_allocate(0, 4096, &coherent) == -1); assert(errno == EACCES && wc_requests == 1 && closed_handles == 3); coherent_error = 0; assert(irisva_msm_allocate(0, 4096, &coherent) == 123); assert(coherent && closed_handles == 4); return 0; } qcom-va-driver-1.1.0/tests/vpp/overlay.cpp000066400000000000000000000532221526764642600205270ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // c++ -O2 -std=c++17 tests/vpp/overlay.cpp -o build/vpp-overlay $(pkg-config --cflags --libs libva // libva-drm) Runs both backends unless one is named on the command line; --bench adds timings. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static void require(bool value, const char *what) { if (!value) { std::fprintf(stderr, "FAIL: %s\n", what); std::exit(1); } } static void va(VAStatus status) { if (status) { std::fprintf(stderr, "VA: %s\n", vaErrorStr(status)); std::exit(1); } } struct Session { int fd = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC); VADisplay display; VAConfigID config; VAContextID context; Session() { require(fd >= 0, "render node"); display = vaGetDisplayDRM(fd); int major, minor; va(vaInitialize(display, &major, &minor)); va(vaCreateConfig(display, VAProfileNone, VAEntrypointVideoProc, nullptr, 0, &config)); va(vaCreateContext(display, config, 0, 0, 0, nullptr, 0, &context)); VAProcPipelineCaps caps{}; va(vaQueryVideoProcPipelineCaps(display, context, nullptr, 0, &caps)); require((caps.blend_flags & (VA_BLEND_GLOBAL_ALPHA | VA_BLEND_PREMULTIPLIED_ALPHA)) == (VA_BLEND_GLOBAL_ALPHA | VA_BLEND_PREMULTIPLIED_ALPHA), "blend caps"); } ~Session() { va(vaDestroyContext(display, context)); va(vaDestroyConfig(display, config)); va(vaTerminate(display)); close(fd); } }; struct Surface { Session &s; VASurfaceID id; unsigned w, h, format; Surface(Session &s, unsigned w, unsigned h, unsigned f) : s(s), w(w), h(h), format(f) { VASurfaceAttrib attr{}; attr.type = VASurfaceAttribPixelFormat; attr.flags = VA_SURFACE_ATTRIB_SETTABLE; attr.value.type = VAGenericValueTypeInteger; attr.value.value.i = f; va(vaCreateSurfaces(s.display, f == VA_FOURCC_P010 ? VA_RT_FORMAT_YUV420_10 : f == VA_FOURCC_NV12 ? VA_RT_FORMAT_YUV420 : VA_RT_FORMAT_RGB32, w, h, &id, 1, &attr, 1)); } ~Surface() { va(vaDestroySurfaces(s.display, &id, 1)); } unsigned bytes() const { return format == VA_FOURCC_NV12 ? 1 : format == VA_FOURCC_P010 ? 2 : 4; } std::vector pixels(const std::vector *upload = nullptr) { VAImage image; va(vaDeriveImage(s.display, id, &image)); void *mapped; va(vaMapBuffer(s.display, image.buf, &mapped)); unsigned rows = bytes() == 4 ? h : h * 3 / 2; std::vector data(w * rows * bytes()); for (unsigned row = 0; row < rows; ++row) { unsigned plane = row >= h; auto *p = static_cast(mapped) + image.offsets[plane] + (row - plane * h) * image.pitches[plane]; if (upload) std::memcpy(p, upload->data() + row * w * bytes(), w * bytes()); else std::memcpy(data.data() + row * w * bytes(), p, w * bytes()); } va(vaUnmapBuffer(s.display, image.buf)); va(vaDestroyImage(s.display, image.image_id)); return data; } }; static void rgb_interop(Surface &surface, const std::vector &pixels) { auto &s = surface.s; std::vector formats(vaMaxNumImageFormats(s.display)); int count = 0; va(vaQueryImageFormats(s.display, formats.data(), &count)); auto found = std::find_if(formats.begin(), formats.begin() + count, [&](const auto &f) { return f.fourcc == surface.format; }); require(found != formats.begin() + count && found->bits_per_pixel == 32, "RGB image format"); VAImage image; va(vaCreateImage(s.display, &*found, surface.w, surface.h, &image)); void *data; va(vaMapBuffer(s.display, image.buf, &data)); for (unsigned y = 0; y < surface.h; ++y) std::memcpy(static_cast(data) + y * image.pitches[0], pixels.data() + y * surface.w * 4, surface.w * 4); va(vaUnmapBuffer(s.display, image.buf)); va(vaPutImage(s.display, surface.id, image.image_id, 0, 0, surface.w, surface.h, 0, 0, surface.w, surface.h)); va(vaGetImage(s.display, surface.id, 0, 0, surface.w, surface.h, image.image_id)); va(vaMapBuffer(s.display, image.buf, &data)); for (unsigned y = 0; y < surface.h; ++y) require(!std::memcmp(static_cast(data) + y * image.pitches[0], pixels.data() + y * surface.w * 4, surface.w * 4), "RGB upload/download"); va(vaUnmapBuffer(s.display, image.buf)); va(vaDestroyImage(s.display, image.image_id)); VADRMPRIMESurfaceDescriptor desc{}; va(vaExportSurfaceHandle(s.display, surface.id, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, VA_EXPORT_SURFACE_READ_ONLY, &desc)); require(desc.num_layers == 1 && desc.layers[0].num_planes == 1, "RGB PRIME export"); VASurfaceAttrib attrs[3]{}; attrs[0].type = VASurfaceAttribPixelFormat; attrs[0].value.type = VAGenericValueTypeInteger; attrs[0].value.value.i = surface.format; attrs[1].type = VASurfaceAttribMemoryType; attrs[1].value.type = VAGenericValueTypeInteger; attrs[1].value.value.i = VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2; attrs[2].type = VASurfaceAttribExternalBufferDescriptor; attrs[2].value.type = VAGenericValueTypePointer; attrs[2].value.value.p = &desc; for (auto &a : attrs) a.flags = VA_SURFACE_ATTRIB_SETTABLE; VASurfaceID imported; va(vaCreateSurfaces(s.display, VA_RT_FORMAT_RGB32, surface.w, surface.h, &imported, 1, attrs, 3)); VADRMPRIMESurfaceDescriptor exported{}; va(vaExportSurfaceHandle(s.display, imported, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, VA_EXPORT_SURFACE_READ_ONLY, &exported)); struct stat original{}, roundtrip{}; require(!fstat(desc.objects[0].fd, &original) && !fstat(exported.objects[0].fd, &roundtrip) && original.st_dev == roundtrip.st_dev && original.st_ino == roundtrip.st_ino, "RGB DMA-BUF identity"); close(desc.objects[0].fd); close(exported.objects[0].fd); va(vaDestroySurfaces(s.display, &surface.id, 1)); surface.id = imported; require(surface.pixels() == pixels, "RGB imported pixels"); } struct Case { const char *name; unsigned w, h, rw, rh; VARectangle crop, out; unsigned format = VA_FOURCC_NV12, rgb = VA_FOURCC_BGRA; bool premult = false, sparse = false, full = false; float alpha = 1; VAProcColorStandardType matrix = VAProcColorStandardBT709; }; static std::vector background(const Case &c) { unsigned bytes = c.format == VA_FOURCC_P010 ? 2 : 1; std::vector data(c.w * c.h * 3 / 2 * bytes); for (unsigned i = 0; i < c.w * c.h * 3 / 2; ++i) { unsigned value = (i * 13 + (i / c.w) * 23) % 220 + 16; if (bytes == 2) reinterpret_cast(data.data())[i] = ((value * 4 + i % 4) & 1023) << 6; else data[i] = value; } return data; } static std::vector foreground(const Case &c) { std::vector data(c.rw * c.rh * 4); for (unsigned y = 0; y < c.rh; ++y) for (unsigned x = 0; x < c.rw; ++x) { auto *p = &data[(y * c.rw + x) * 4]; p[3] = c.sparse ? ((x % 80 < 40 && y % 40 < 6) ? 200 : 0) : (x * 19 + y * 37) % 256; p[0] = (x * 3 + y * 5) % 256; p[1] = (x * 13 + y * 7) % 256; p[2] = (x * 17 + y * 11) % 256; if (c.premult) for (int k = 0; k < 3; ++k) p[k] = (unsigned(p[k]) * p[3] + 127) / 255; } return data; } using Pixel = std::array; static Pixel sample(const Case &c, const std::vector &rgb, int x, int y) { Pixel result{}; x -= c.out.x; y -= c.out.y; if (x < 0 || y < 0 || x >= c.out.width || y >= c.out.height) return result; double px = std::clamp((x + .5) * c.crop.width / c.out.width - .5, 0.0, double(c.crop.width - 1)); double py = std::clamp((y + .5) * c.crop.height / c.out.height - .5, 0.0, double(c.crop.height - 1)); int ix = int(px), iy = int(py); double fx = px - ix, fy = py - iy; for (int row = 0; row < 2; ++row) for (int col = 0; col < 2; ++col) { int sx = c.crop.x + std::min(ix + col, int(c.crop.width) - 1), sy = c.crop.y + std::min(iy + row, int(c.crop.height) - 1); auto p = &rgb[(sy * c.rw + sx) * 4]; double a = p[3] / 255.0; double weight = (row ? fy : 1 - fy) * (col ? fx : 1 - fx) * c.alpha; result[3] += a * weight; for (int channel = 0; channel < 3; ++channel) result[channel] += p[c.rgb == VA_FOURCC_BGRA ? 2 - channel : channel] / 255.0 * (c.premult ? 1 : a) * weight; } return result; } static int reference_error(const Case &c, const std::vector &base, const std::vector &rgb, const std::vector &result) { bool wide = c.format == VA_FOURCC_P010; int scale = wide ? 4 : 1, maximum = wide ? 1023 : 255, error = 0; auto load = [&](const std::vector &v, unsigned i) { return wide ? reinterpret_cast(v.data())[i] >> 6 : v[i]; }; double kr = c.matrix == VAProcColorStandardBT601 ? .299 : c.matrix == VAProcColorStandardBT2020 ? .2627 : .2126; double kb = c.matrix == VAProcColorStandardBT601 ? .114 : c.matrix == VAProcColorStandardBT2020 ? .0593 : .0722; for (unsigned y = 0; y < c.h; y += 2) for (unsigned x = 0; x < c.w; x += 2) { double sum_a = 0, sum_u = 0, sum_v = 0; for (unsigned row = 0; row < 2; ++row) for (unsigned col = 0; col < 2; ++col) { auto p = sample(c, rgb, x + col, y + row); double yy = kr * p[0] + (1 - kr - kb) * p[1] + kb * p[2]; unsigned index = (y + row) * c.w + x + col; double value = load(base, index) * (1 - p[3]) + scale * ((c.full ? 0 : 16) * p[3] + (c.full ? 255 : 219) * yy); error = std::max(error, std::abs(load(result, index) - std::clamp(int(std::lround(value)), 0, maximum))); sum_a += p[3]; sum_u += (p[2] - yy) / (2 * (1 - kb)); sum_v += (p[0] - yy) / (2 * (1 - kr)); } for (unsigned channel = 0; channel < 2; ++channel) { unsigned index = c.w * c.h + (y / 2) * c.w + x + channel; double value = load(base, index) * (1 - sum_a / 4) + scale * (128 * sum_a / 4 + (c.full ? 255 : 224) * (channel ? sum_v : sum_u) / 4); error = std::max(error, std::abs(load(result, index) - std::clamp(int(std::lround(value)), 0, maximum))); } } return error; } struct Pipeline { Session &s; std::vector buffers; VARectangle crop, out; VABlendState blend{}; Pipeline(Session &s, Surface &base, Surface &rgb, const Case &c) : s(s), crop(c.crop), out(c.out) { VAProcPipelineParameterBuffer p{}; p.surface = base.id; p.surface_color_standard = p.output_color_standard = c.matrix; p.input_color_properties.color_range = p.output_color_properties.color_range = c.full ? VA_SOURCE_RANGE_FULL : VA_SOURCE_RANGE_REDUCED; add(p); p.surface = rgb.id; p.surface_region = &crop; p.output_region = &out; blend.flags = VA_BLEND_GLOBAL_ALPHA | (c.premult ? VA_BLEND_PREMULTIPLIED_ALPHA : 0); blend.global_alpha = c.alpha; p.blend_state = &blend; add(p); } void add(VAProcPipelineParameterBuffer &p) { VABufferID id; va(vaCreateBuffer(s.display, s.context, VAProcPipelineParameterBufferType, sizeof(p), 1, &p, &id)); buffers.push_back(id); } void submit(Surface &output, bool base_only = false, bool snapshot = false) { va(vaBeginPicture(s.display, s.context, output.id)); // FFmpeg may submit the layers in separate calls. for (size_t i = 0; i < (base_only ? 1 : buffers.size()); ++i) va(vaRenderPicture(s.display, s.context, &buffers[i], 1)); auto saved_crop = crop, saved_out = out; auto saved_blend = blend; if (snapshot) { crop = {0, 0, 1, 1}; out = {0, 0, 1, 1}; blend.global_alpha = 0; } va(vaEndPicture(s.display, s.context)); crop = saved_crop; out = saved_out; blend = saved_blend; va(vaSyncSurface(s.display, output.id)); } ~Pipeline() { for (auto id : buffers) va(vaDestroyBuffer(s.display, id)); } }; static void run(Session &s, const Case &c, const std::vector &backends, int rounds) { Surface base(s, c.w, c.h, c.format), rgb(s, c.rw, c.rh, c.rgb); auto b = background(c), r = foreground(c); base.pixels(&b); if (rounds == 3) rgb_interop(rgb, r); else rgb.pixels(&r); Pipeline pipeline(s, base, rgb, c); std::vector previous; for (auto backend : backends) { setenv("IRIS_VAAPI_OVERLAY", backend, 1); Surface output(s, c.w, c.h, c.format); pipeline.submit(output); auto data = output.pixels(); int error = reference_error(c, b, r, data); if (error > (c.format == VA_FOURCC_P010 ? 5 : 2)) { std::fprintf(stderr, "%s %s max_error=%d\n", c.name, backend, error); if (!previous.empty()) for (size_t j = 0, count = 0; j < data.size() && count < 20; ++j) if (data[j] != previous[j]) { std::fprintf(stderr, "offset=%zu got=%u want=%u\n", j, data[j], previous[j]); ++count; } } require(error <= (c.format == VA_FOURCC_P010 ? 5 : 2), "double precision blend reference"); if (!previous.empty()) require(data == previous, "CDSP/Vulkan pixel equality"); previous = data; if (rounds == 3) { pipeline.submit(output, true); require(output.pixels() == b, "base-only copy after overlay"); pipeline.submit(output, false, true); require(output.pixels() == data, "snapshot caller rectangles and blend state"); unsigned flags = pipeline.blend.flags; pipeline.blend.flags = VA_BLEND_LUMA_KEY; va(vaBeginPicture(s.display, s.context, output.id)); va(vaRenderPicture(s.display, s.context, &pipeline.buffers[0], 1)); require(vaRenderPicture(s.display, s.context, &pipeline.buffers[1], 1) != VA_STATUS_SUCCESS, "reject luma key"); require(vaEndPicture(s.display, s.context) != VA_STATUS_SUCCESS, "reject partial composition"); pipeline.blend.flags = flags; require(output.pixels() == data, "rejected composition leaves output unchanged"); VAProcPipelineParameterBuffer p{}; p.surface = rgb.id; p.surface_region = &pipeline.crop; p.output_region = &pipeline.out; p.blend_state = &pipeline.blend; pipeline.add(p); pipeline.submit(output); auto twice = output.pixels(); require(reference_error(c, data, r, twice) <= (c.format == VA_FOURCC_P010 ? 5 : 2), "multiple overlay layers"); va(vaDestroyBuffer(s.display, pipeline.buffers.back())); pipeline.buffers.pop_back(); } for (int i = 0; i < 8; ++i) pipeline.submit(output); std::vector times; for (int i = 0; i < rounds; ++i) { auto begin = std::chrono::steady_clock::now(); pipeline.submit(output); times.push_back( std::chrono::duration(std::chrono::steady_clock::now() - begin) .count()); } std::sort(times.begin(), times.end()); std::printf("%-18s %-6s max_error=%d median=%.3f ms p95=%.3f ms\n", c.name, backend, error, times[times.size() / 2], times[times.size() * 95 / 100]); std::fflush(stdout); } } static void geometry(Session &s, const std::vector &backends) { Case c{"geometry", 64, 48, 32, 16, {0, 0, 32, 16}, {9, 7, 51, 27}}; Case larger = c; larger.w = 128; larger.h = 96; Surface base(s, 128, 96, c.format), rgb(s, c.rw, c.rh, c.rgb), output(s, c.w, c.h, c.format); auto b = background(larger), r = foreground(c); base.pixels(&b); rgb.pixels(&r); Pipeline pipeline(s, base, rgb, c); pipeline.submit(output, true); auto resized = output.pixels(); for (auto backend : backends) { setenv("IRIS_VAAPI_OVERLAY", backend, 1); pipeline.submit(output); require(reference_error(c, resized, r, output.pixels()) <= 2, "scale base and overlay in one submission"); } std::puts("PASS combined base scaling and overlay"); } int main(int argc, char **argv) { bool bench = false, with_geometry = false; std::vector backends; for (int i = 1; i < argc; ++i) { if (!std::strcmp(argv[i], "--bench")) bench = true; else if (!std::strcmp(argv[i], "--geometry")) with_geometry = true; else backends.push_back(argv[i]); } if (backends.empty()) backends = {"cdsp", "vulkan"}; Session s; std::vector cases = { {"straight", 128, 96, 64, 48, {0, 0, 64, 48}, {32, 24, 64, 48}}, {"odd-crop-scale", 130, 98, 67, 51, {3, 5, 61, 43}, {-5, 7, 119, 85}}, {"clipped", 130, 98, 67, 51, {0, 0, 67, 51}, {121, 91, 67, 51}}, {"offscreen", 128, 96, 64, 48, {0, 0, 64, 48}, {-100, -100, 64, 48}}, {"p010", 128, 96, 64, 48, {0, 0, 64, 48}, {16, 10, 64, 48}, VA_FOURCC_P010}, {"premult-full601", 130, 98, 67, 51, {1, 1, 65, 49}, {5, 3, 121, 91}, VA_FOURCC_NV12, VA_FOURCC_RGBA, true, false, true, .625f, VAProcColorStandardBT601}, {"bt2020-alpha", 128, 96, 64, 48, {0, 0, 64, 48}, {10, 11, 64, 48}, VA_FOURCC_P010, VA_FOURCC_RGBA, false, false, false, .5f, VAProcColorStandardBT2020}, {"p010-scaled", 130, 98, 67, 51, {2, 1, 63, 49}, {3, 7, 119, 85}, VA_FOURCC_P010, VA_FOURCC_BGRA, false, false, false, .3f}, {"downscale", 128, 96, 93, 71, {0, 0, 93, 71}, {5, 9, 37, 29}}, {"narrow", 128, 96, 7, 3, {1, 0, 5, 3}, {-1, 15, 111, 51}}, {"transparent", 128, 96, 64, 48, {0, 0, 64, 48}, {32, 24, 64, 48}, VA_FOURCC_NV12, VA_FOURCC_BGRA, false, false, false, 0.f}}; for (const auto &c : cases) run(s, c, backends, 3); if (with_geometry) geometry(s, backends); if (bench) { std::vector timings = { {"1080p-subtitle", 1920, 1080, 800, 160, {0, 0, 800, 160}, {560, 880, 800, 160}, VA_FOURCC_NV12, VA_FOURCC_BGRA, false, true}, {"4k-subtitle", 3840, 2160, 1600, 320, {0, 0, 1600, 320}, {1120, 1760, 1600, 320}, VA_FOURCC_NV12, VA_FOURCC_BGRA, false, true}, {"4k-full-sparse", 3840, 2160, 3840, 2160, {0, 0, 3840, 2160}, {0, 0, 3840, 2160}, VA_FOURCC_NV12, VA_FOURCC_BGRA, false, true}, {"1080p-full", 1920, 1080, 1920, 1080, {0, 0, 1920, 1080}, {0, 0, 1920, 1080}}, {"4k-full", 3840, 2160, 3840, 2160, {0, 0, 3840, 2160}, {0, 0, 3840, 2160}}, {"4k-scaled-subtitle", 3840, 2160, 800, 160, {0, 0, 800, 160}, {1120, 1760, 1600, 320}, VA_FOURCC_NV12, VA_FOURCC_BGRA, false, true}, {"4k-p010-subtitle", 3840, 2160, 1600, 320, {0, 0, 1600, 320}, {1120, 1760, 1600, 320}, VA_FOURCC_P010, VA_FOURCC_BGRA, false, true}}; for (const auto &c : timings) run(s, c, backends, 100); } std::puts("PASS overlay"); } qcom-va-driver-1.1.0/tests/vpp/scaling.cpp000066400000000000000000001244771526764642600205010ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Hardware integration: scalar reference, PRIME2 identity and DMA-BUF lifetime. // Pass cdsp or vulkan to select a backend; --bench runs warmed VAAPI timings. #include #include #include #include #include #include "../../src/cdsp/filter.h" #ifndef VPP_NO_CDSP #include "../../src/cdsp/client.h" #endif #include #include #ifndef VPP_NO_CDSP #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static int input_fd = -1, output_fd = -1, direct_calls = 0; static int maps = 0, unmaps = 0; static bool gpu = false, benchmark = false; static unsigned imports = 0; static bool saw_input = false, saw_output = false; static bool same_buffer(int a, int b) { struct stat x{}, y{}; return a >= 0 && b >= 0 && !fstat(a, &x) && !fstat(b, &y) && x.st_dev == y.st_dev && x.st_ino == y.st_ino; } extern "C" VkResult vkAllocateMemory(VkDevice device, const VkMemoryAllocateInfo *info, const VkAllocationCallbacks *callbacks, VkDeviceMemory *memory) { static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "vkAllocateMemory")); for (auto next = static_cast(info->pNext); next; next = next->pNext) if (next->sType == VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR) { auto fd = reinterpret_cast(next); if (fd->handleType == VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT) { ++imports; saw_input |= same_buffer(fd->fd, input_fd); saw_output |= same_buffer(fd->fd, output_fd); } } return real(device, info, callbacks, memory); } extern "C" int ioctl(int fd, unsigned long request, ...) noexcept { va_list args; va_start(args, request); void *arg = va_arg(args, void *); va_end(args); static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "ioctl")); bool both = false; #ifndef VPP_NO_CDSP if (request == FASTRPC_IOCTL_INVOKE && arg) { auto *call = static_cast(arg); auto *buffers = reinterpret_cast(call->args); unsigned n = ((call->sc >> 16) & 255) + ((call->sc >> 8) & 255) + ((call->sc >> 4) & 15) + (call->sc & 15); bool in = false, out = false; for (unsigned i = 0; i < n; ++i) { in |= same_buffer(buffers[i].fd, input_fd); out |= same_buffer(buffers[i].fd, output_fd); } both = in && out; } #endif int result = real(fd, request, arg); #ifndef VPP_NO_CDSP if (!result && request == FASTRPC_IOCTL_MEM_MAP) ++maps; if (!result && request == FASTRPC_IOCTL_MEM_UNMAP) ++unmaps; #endif if (!result && both) ++direct_calls; return result; } static void require(bool ok, const char *message) { if (!ok) { std::fprintf(stderr, "FAIL: %s\n", message); std::exit(1); } } static void va(VAStatus status) { if (status) { std::fprintf(stderr, "VA: %s\n", vaErrorStr(status)); std::exit(1); } } static void sync(int fd, uint64_t flags) { dma_buf_sync s{flags}; require(ioctl(fd, DMA_BUF_IOCTL_SYNC, &s) == 0, "DMA-BUF CPU sync"); } struct Allocation { VADRMPRIMESurfaceDescriptor desc{}; uint8_t *data; Allocation(unsigned w, unsigned h, unsigned format = VA_FOURCC_NV12, bool tight = false) { auto &obj = desc.objects[0]; auto &l = desc.layers[0]; desc.fourcc = format; l.drm_format = format == VA_FOURCC_P010 ? DRM_FORMAT_P010 : DRM_FORMAT_NV12; desc.width = w; desc.height = h; desc.num_objects = desc.num_layers = 1; l.num_planes = 2; unsigned row_bytes = w * (format == VA_FOURCC_P010 ? 2 : 1); l.pitch[0] = l.pitch[1] = tight ? row_bytes : (row_bytes + 127) & ~127u; // Exercise both padded imports and planes ending at the DMA-BUF boundary. l.offset[0] = tight ? 0 : 128; l.offset[1] = l.offset[0] + l.pitch[0] * (tight ? h : (h + 31) & ~31u); obj.size = (l.offset[1] + l.pitch[1] * ((h + 1) / 2) + 4095) & ~4095u; obj.drm_format_modifier = DRM_FORMAT_MOD_LINEAR; int heap = open("/dev/dma_heap/system", O_RDONLY | O_CLOEXEC); require(heap >= 0, "open system heap"); dma_heap_allocation_data a{}; a.len = obj.size; a.fd_flags = O_RDWR | O_CLOEXEC; require(!ioctl(heap, DMA_HEAP_IOCTL_ALLOC, &a), "allocate DMA-BUF"); close(heap); obj.fd = a.fd; data = static_cast( mmap(nullptr, obj.size, PROT_READ | PROT_WRITE, MAP_SHARED, obj.fd, 0)); require(data != MAP_FAILED, "map allocation"); } ~Allocation() { munmap(data, desc.objects[0].size); close(desc.objects[0].fd); } }; static VASurfaceID import(VADisplay display, VADRMPRIMESurfaceDescriptor &desc) { VASurfaceAttrib attrs[2]{}; attrs[0].type = VASurfaceAttribMemoryType; attrs[0].flags = VA_SURFACE_ATTRIB_SETTABLE; attrs[0].value.type = VAGenericValueTypeInteger; attrs[0].value.value.i = VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2; attrs[1].type = VASurfaceAttribExternalBufferDescriptor; attrs[1].flags = VA_SURFACE_ATTRIB_SETTABLE; attrs[1].value.type = VAGenericValueTypePointer; auto owned = desc; owned.objects[0].fd = dup(desc.objects[0].fd); require(owned.objects[0].fd >= 0, "duplicate descriptor fd"); attrs[1].value.value.p = &owned; VASurfaceID id; va(vaCreateSurfaces( display, desc.fourcc == VA_FOURCC_P010 ? VA_RT_FORMAT_YUV420_10 : VA_RT_FORMAT_YUV420, desc.width, desc.height, &id, 1, attrs, 2)); close(owned.objects[0].fd); // Driver must own its own fd after import. return id; } struct Case { unsigned sw, sh, dw, dh; bool crop; unsigned src_format = VA_FOURCC_NV12, dst_format = VA_FOURCC_NV12; unsigned rotation = VA_ROTATION_NONE; bool tight = false; }; static void run_case(Case c) { Allocation input(c.sw, c.sh, c.src_format, c.tight), reference(c.dw, c.dh, c.dst_format), destination(c.dw, c.dh, c.dst_format, c.tight); bool wide = c.src_format == VA_FOURCC_P010 || c.dst_format == VA_FOURCC_P010; auto &sl = input.desc.layers[0]; sync(input.desc.objects[0].fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE); std::memset(input.data, 0xa5, input.desc.objects[0].size); for (unsigned p = 0; p < 2; ++p) for (unsigned y = 0; y < (p ? c.sh / 2 : c.sh); ++y) for (unsigned x = 0; x < c.sw; ++x) if (c.src_format == VA_FOURCC_P010) reinterpret_cast(input.data + sl.offset[p] + y * sl.pitch[p])[x] = ((x * 37 + y * 53 + (p ? 257 : 0)) & 1023) << 6; else input.data[sl.offset[p] + y * sl.pitch[p] + x] = wide ? (x * 37 + y * 53 + p * 71) & 255 : p ? (x & 1 ? 150 + (x / 2 + y) % 80 : 20 + (x / 2 + y) % 80) : 16 + (x / 8 + y / 4) % 220; sync(input.desc.objects[0].fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE); VARectangle region{static_cast(c.crop ? 16 : 0), static_cast(c.crop ? 2 : 0), static_cast(c.crop ? 608 : c.sw), static_cast(c.crop ? 360 : c.sh)}; sync(input.desc.objects[0].fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ); sync(reference.desc.objects[0].fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE); unsigned scaled_width = (c.rotation & 1) ? c.dh : c.dw; unsigned scaled_height = (c.rotation & 1) ? c.dw : c.dh; bool enlarging = region.width < scaled_width || region.height < scaled_height; std::vector row(region.width); for (unsigned p = 0; p < 2; ++p) { int channels = p ? 2 : 1; const auto *src = input.data + sl.offset[p] + (region.y / channels) * sl.pitch[p] + region.x * (c.src_format == VA_FOURCC_P010 ? 2 : 1); auto *dst = reference.data + reference.desc.layers[0].offset[p]; for (unsigned y = 0; y < scaled_height / channels; ++y) { auto fy = make_filter(region.height / channels, scaled_height / channels, y); for (unsigned x = 0; x < region.width; ++x) { int sum = 0; for (int k = 0; k < fy.tap_count; ++k) { const auto *line = src + fy.indices[k] * sl.pitch[p]; int sample = c.src_format == VA_FOURCC_P010 ? reinterpret_cast(line)[x] >> 6 : line[x] << (wide ? 2 : 0); sum += sample * fy.weights[k]; } if (wide) row[x] = (sum + (1 << (fy.shift - 5))) >> (fy.shift - 4); else row[x] = enlarging ? (sum + (1 << (fy.shift - 7))) >> (fy.shift - 6) : round_clip_u8(sum, fy.shift); } for (unsigned x = 0; x < scaled_width / channels; ++x) { auto fx = make_filter(region.width / channels, scaled_width / channels, x); for (int ch = 0; ch < channels; ++ch) { int sum = 0; for (int k = 0; k < fx.tap_count; ++k) sum += row[fx.indices[k] * channels + ch] * fx.weights[k]; unsigned ox = x, oy = y; if (c.rotation == VA_ROTATION_90) { ox = scaled_height / channels - 1 - y; oy = x; } else if (c.rotation == VA_ROTATION_180) { ox = scaled_width / channels - 1 - x; oy = scaled_height / channels - 1 - y; } else if (c.rotation == VA_ROTATION_270) { ox = y; oy = scaled_width / channels - 1 - x; } auto *line = dst + oy * reference.desc.layers[0].pitch[p]; int shift = fx.shift + (wide ? 4 : enlarging ? 6 : 0); if (c.dst_format == VA_FOURCC_P010) reinterpret_cast(line)[ox * channels + ch] = clamp_int((sum + (1 << (shift - 1))) >> shift, 0, 1023) << 6; else line[ox * channels + ch] = round_clip_u8(sum, shift + (wide ? 2 : 0)); } } } } sync(reference.desc.objects[0].fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE); sync(input.desc.objects[0].fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ); int drm = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC); require(drm >= 0, "open DRM"); VADisplay display = vaGetDisplayDRM(drm); int major, minor; va(vaInitialize(display, &major, &minor)); VAConfigAttrib rt{VAConfigAttribRTFormat, 0}; va(vaGetConfigAttributes(display, VAProfileNone, VAEntrypointVideoProc, &rt, 1)); require(rt.value == (VA_RT_FORMAT_YUV420 | VA_RT_FORMAT_YUV420_10 | VA_RT_FORMAT_RGB32), "VPP render formats"); VAConfigID config; VAContextID context; rt.value = c.dst_format == VA_FOURCC_P010 ? VA_RT_FORMAT_YUV420_10 : VA_RT_FORMAT_YUV420; va(vaCreateConfig(display, VAProfileNone, VAEntrypointVideoProc, &rt, 1, &config)); unsigned attribute_count = 0; va(vaQuerySurfaceAttributes(display, config, nullptr, &attribute_count)); std::vector attributes(attribute_count); va(vaQuerySurfaceAttributes(display, config, attributes.data(), &attribute_count)); unsigned formats = 0; for (const auto &a : attributes) if (a.type == VASurfaceAttribPixelFormat) formats |= a.value.value.i == VA_FOURCC_NV12 ? 1 : a.value.value.i == VA_FOURCC_P010 ? 2 : 0; require(formats == 3, "VPP surface formats"); va(vaCreateContext(display, config, 0, 0, 0, nullptr, 0, &context)); VAProcFilterType filters[VAProcFilterCount]{}; unsigned n = VAProcFilterCount; va(vaQueryVideoProcFilters(display, context, filters, &n)); require(n == 0 || (n == 1 && filters[0] == VAProcFilterHighDynamicRangeToneMapping), "available VPP filters"); VAProcPipelineCaps caps{}; va(vaQueryVideoProcPipelineCaps(display, context, nullptr, 0, &caps)); require(caps.num_input_pixel_formats == 4 && caps.num_output_pixel_formats == 2, "VPP input/output format count"); require(caps.input_pixel_format && caps.input_pixel_format[0] == VA_FOURCC_NV12 && caps.input_pixel_format[1] == VA_FOURCC_P010 && caps.output_pixel_format[1] == VA_FOURCC_P010, "NV12/P010 capability values"); require(caps.rotation_flags == ((1 << VA_ROTATION_NONE) | (1 << VA_ROTATION_90) | (1 << VA_ROTATION_180) | (1 << VA_ROTATION_270)), "rotation capabilities"); auto in = import(display, input.desc); // Test composed and separate-layer PRIME2 imports. auto separate = destination.desc; separate.num_layers = 2; separate.layers[0].drm_format = c.dst_format == VA_FOURCC_P010 ? DRM_FORMAT_R16 : DRM_FORMAT_R8; separate.layers[0].num_planes = 1; separate.layers[1].drm_format = c.dst_format == VA_FOURCC_P010 ? DRM_FORMAT_GR1616 : DRM_FORMAT_GR88; separate.layers[1].num_planes = 1; separate.layers[1].pitch[0] = destination.desc.layers[0].pitch[1]; separate.layers[1].offset[0] = destination.desc.layers[0].offset[1]; VASurfaceID outputs[3]; outputs[0] = import(display, separate); va(vaCreateSurfaces( display, c.dst_format == VA_FOURCC_P010 ? VA_RT_FORMAT_YUV420_10 : VA_RT_FORMAT_YUV420, c.dw, c.dh, &outputs[1], 2, nullptr, 0)); int initial_maps = maps, initial_unmaps = unmaps; unsigned initial_imports = imports; saw_input = false; for (unsigned kind = 0; kind < 3; ++kind) { VADRMPRIMESurfaceDescriptor exported{}; // kind 0: external heap; kind 1: pre-exported MSM; kind 2: lazy VPP heap. if (kind != 2) { va(vaExportSurfaceHandle( display, outputs[kind], VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_COMPOSED_LAYERS, &exported)); output_fd = exported.objects[0].fd; } else output_fd = -1; input_fd = input.desc.objects[0].fd; direct_calls = 0; saw_output = false; for (int repeat = 0; repeat < 3; ++repeat) { VAProcPipelineParameterBuffer params{}; params.surface = in; params.rotation_state = c.rotation; params.surface_region = ®ion; VABufferID buffer; va(vaCreateBuffer(display, context, VAProcPipelineParameterBufferType, sizeof(params), 1, ¶ms, &buffer)); va(vaBeginPicture(display, context, outputs[kind])); va(vaRenderPicture(display, context, &buffer, 1)); va(vaDestroyBuffer(display, buffer)); // Parameters must already be snapshotted. va(vaEndPicture(display, context)); va(vaSyncSurface(display, outputs[kind])); if (kind == 2 && repeat == 0) { va(vaExportSurfaceHandle( display, outputs[kind], VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_COMPOSED_LAYERS, &exported)); output_fd = exported.objects[0].fd; } } if (gpu) { require(imports - initial_imports == kind + 2, "persistent Vulkan imports"); require(saw_input && (kind == 2 || saw_output), "original DMA-BUFs reach Vulkan"); require(maps == initial_maps && !direct_calls, "Vulkan processing does not call DSP"); } else { require(maps - initial_maps == int(kind) + 2, "one persistent map per source/destination"); require(unmaps == initial_unmaps, "maps survive across frames"); require(direct_calls == (kind == 2 ? 2 : 3), "both original DMA-BUFs must reach successful FastRPC calls"); } if (benchmark && kind == 0) { VAProcPipelineParameterBuffer params{}; params.surface = in; params.surface_region = ®ion; params.rotation_state = c.rotation; VABufferID buffer; va(vaCreateBuffer(display, context, VAProcPipelineParameterBufferType, sizeof(params), 1, ¶ms, &buffer)); auto submit = [&] { va(vaBeginPicture(display, context, outputs[0])); va(vaRenderPicture(display, context, &buffer, 1)); va(vaEndPicture(display, context)); va(vaSyncSurface(display, outputs[0])); }; auto warm = std::chrono::steady_clock::now(); do { submit(); } while ( std::chrono::duration(std::chrono::steady_clock::now() - warm).count() < 1); std::vector times; for (int i = 0; i < 100; ++i) { auto begin = std::chrono::steady_clock::now(); submit(); times.push_back(std::chrono::duration( std::chrono::steady_clock::now() - begin) .count()); } std::sort(times.begin(), times.end()); std::printf( "BENCH %s %s->%s %ux%u->%ux%u crop=%d rotation=%u median=%.3f ms p95=%.3f ms\n", gpu ? "vulkan" : "cdsp", c.src_format == VA_FOURCC_P010 ? "P010" : "NV12", c.dst_format == VA_FOURCC_P010 ? "P010" : "NV12", c.sw, c.sh, c.dw, c.dh, c.crop, c.rotation * 90, times[50], times[95]); va(vaDestroyBuffer(display, buffer)); } VAImage image{}; va(vaDeriveImage(display, outputs[kind], &image)); void *mapped; va(vaMapBuffer(display, image.buf, &mapped)); sync(reference.desc.objects[0].fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ); unsigned maxdiff[2]{}; for (unsigned p = 0; p < 2; ++p) for (unsigned y = 0; y < (p ? c.dh / 2 : c.dh); ++y) for (unsigned x = 0; x < c.dw; ++x) { const auto *actual = static_cast(mapped) + image.offsets[p] + y * image.pitches[p]; const auto *expected = reference.data + reference.desc.layers[0].offset[p] + y * reference.desc.layers[0].pitch[p]; int a = c.dst_format == VA_FOURCC_P010 ? reinterpret_cast(actual)[x] >> 6 : actual[x]; int b = c.dst_format == VA_FOURCC_P010 ? reinterpret_cast(expected)[x] >> 6 : expected[x]; if (c.dst_format == VA_FOURCC_P010) require(!(reinterpret_cast(actual)[x] & 63), "P010 low bits zero"); maxdiff[p] = std::max(maxdiff[p], unsigned(std::abs(a - b))); } sync(reference.desc.objects[0].fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ); va(vaUnmapBuffer(display, image.buf)); va(vaDestroyImage(display, image.image_id)); std::printf( "%s -> %s %ux%u -> %ux%u crop=%d rotation=%u output=%u RPC=%d maxdiff Y=%u UV=%u\n", c.src_format == VA_FOURCC_P010 ? "P010" : "NV12", c.dst_format == VA_FOURCC_P010 ? "P010" : "NV12", c.sw, c.sh, c.dw, c.dh, c.crop, c.rotation * 90, kind, direct_calls, maxdiff[0], maxdiff[1]); require(maxdiff[0] <= unsigned(wide ? 0 : 1) && maxdiff[1] <= unsigned(wide ? 0 : 1), "VPP matches CPU reference within rounding tolerance"); close(exported.objects[0].fd); } // Unsupported operations must fail before a DSP invocation. VAProcPipelineParameterBuffer bad{}; bad.surface = in; bad.rotation_state = VA_ROTATION_270 + 1; VABufferID buffer; va(vaCreateBuffer(display, context, VAProcPipelineParameterBufferType, sizeof(bad), 1, &bad, &buffer)); va(vaBeginPicture(display, context, outputs[0])); require(vaRenderPicture(display, context, &buffer, 1) == VA_STATUS_ERROR_INVALID_PARAMETER, "reject invalid rotation"); require(vaEndPicture(display, context) == VA_STATUS_ERROR_INVALID_PARAMETER, "reject missing parameters"); va(vaDestroyBuffer(display, buffer)); auto alias = import(display, input.desc); bad = {}; bad.surface = in; va(vaCreateBuffer(display, context, VAProcPipelineParameterBufferType, sizeof(bad), 1, &bad, &buffer)); va(vaBeginPicture(display, context, alias)); va(vaRenderPicture(display, context, &buffer, 1)); int before = direct_calls; require(vaEndPicture(display, context) == VA_STATUS_ERROR_INVALID_SURFACE, "reject DMA-BUF alias"); require(direct_calls == before, "alias must not reach DSP"); va(vaDestroyBuffer(display, buffer)); va(vaDestroySurfaces(display, &alias, 1)); va(vaDestroyContext(display, context)); if (!gpu) require(unmaps - initial_unmaps == 4, "last context retires all persistent maps"); // Surfaces outlive the last context, then get mapped into a new session. va(vaCreateContext(display, config, 0, 0, 0, nullptr, 0, &context)); VAProcPipelineParameterBuffer again{}; again.surface = in; again.surface_region = ®ion; again.rotation_state = c.rotation; va(vaCreateBuffer(display, context, VAProcPipelineParameterBufferType, sizeof(again), 1, &again, &buffer)); va(vaBeginPicture(display, context, outputs[0])); va(vaRenderPicture(display, context, &buffer, 1)); va(vaEndPicture(display, context)); va(vaDestroyBuffer(display, buffer)); if (gpu) require(imports - initial_imports == 4, "imports outlive their VA context"); else require(maps - initial_maps == 6, "retained buffers remap into a new context"); va(vaDestroyContext(display, context)); if (!gpu) require(unmaps - initial_unmaps == 6, "recreated context cleans up maps"); va(vaDestroySurfaces(display, &in, 1)); va(vaDestroySurfaces(display, outputs, 3)); va(vaDestroyConfig(display, config)); va(vaTerminate(display)); close(drm); } #ifndef VPP_NO_CDSP static cdsp_vpp_buffer *import_test_buffer(size_t size) { int heap = open("/dev/dma_heap/system", O_RDONLY | O_CLOEXEC); require(heap >= 0, "heap open"); dma_heap_allocation_data a{}; a.len = (size + 4095) & ~4095; a.fd_flags = O_RDWR | O_CLOEXEC; require(ioctl(heap, DMA_HEAP_IOCTL_ALLOC, &a) == 0, "heap allocate"); close(heap); cdsp_vpp_buffer *b = nullptr; // Deliberately import a size that is not a page multiple. require(cdsp_vpp_import(a.fd, size, &b) == 0, "import"); close(a.fd); return b; } static std::vector pixels(const cdsp_vpp_frame &f, size_t size, int value) { std::vector data(size, 0xa5); for (unsigned p = 0; p < 2; p++) for (unsigned y = 0; y < f.height / (p ? 2 : 1); y++) for (unsigned x = 0; x < f.width; x++) data[(p ? f.uv_offset : f.y_offset) + y * f.stride + x] = p ? ((x & 1) ? 255 - value : value / 2) : value; return data; } static void check_buffer_lifetime() { struct Profile { uint64_t before = UINT64_C(0x123456789abcdef0); uint64_t counters[CDSP_VPP_PROFILE_COUNTERS]{}; uint64_t after = UINT64_C(0x123456789abcdef0); } profile; constexpr size_t source_size = 4081, dest_size = 12271; // Import before opening the first context: mapping must remain lazy. auto *s = import_test_buffer(source_size), *d = import_test_buffer(dest_size); cdsp_vpp_frame src{s, 80, 18, 104, 16, 16 + 104 * 20}; cdsp_vpp_frame dst{d, 160, 36, 184, 16, 16 + 184 * 38}; auto *sp = (uint8_t *)cdsp_vpp_buffer_map(s); auto *dp = (uint8_t *)cdsp_vpp_buffer_map(d); constexpr int cycles = 2, frames = 3; for (int cycle = 0; cycle < cycles; cycle++) { cdsp_vpp_context *ctx = nullptr; require(cdsp_vpp_create(&ctx, CDSP_VPP_MAX_WORKERS, 1) == 0, "context create"); require(cdsp_vpp_profile(ctx, 1, profile.counters) == 0, "enable profiling"); for (int frame = 0; frame < frames; frame++) { int value = (cycle * 31 + frame * 17) & 255; auto input = pixels(src, source_size, value); auto expected = pixels(dst, dest_size, value); sync(cdsp_vpp_buffer_fd(s), DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE); memcpy(sp, input.data(), source_size); sync(cdsp_vpp_buffer_fd(s), DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE); sync(cdsp_vpp_buffer_fd(d), DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE); memset(dp, 0xa5, dest_size); sync(cdsp_vpp_buffer_fd(d), DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE); uint64_t us; require(cdsp_vpp_scale(ctx, &src, nullptr, &dst, &us) == 0, "persistent scale"); sync(cdsp_vpp_buffer_fd(d), DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ); require(memcmp(dp, expected.data(), dest_size) == 0, "pixels and all padding"); sync(cdsp_vpp_buffer_fd(d), DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ); sync(cdsp_vpp_buffer_fd(s), DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ); require(memcmp(sp, input.data(), source_size) == 0, "source unchanged"); sync(cdsp_vpp_buffer_fd(s), DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ); // A second import of the same source gets its own mapping lifetime. cdsp_vpp_buffer *alias = nullptr; require(cdsp_vpp_import(cdsp_vpp_buffer_fd(s), source_size, &alias) == 0, "reimport source"); auto aliased_src = src; aliased_src.buffer = alias; require(cdsp_vpp_scale(ctx, &aliased_src, nullptr, &dst, &us) == 0, "scale reimported source"); sync(cdsp_vpp_buffer_fd(d), DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ); require(memcmp(dp, expected.data(), dest_size) == 0, "reimported pixels"); sync(cdsp_vpp_buffer_fd(d), DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ); cdsp_vpp_release(alias); } require(cdsp_vpp_profile(ctx, 0, profile.counters) == 0, "read all worker counters"); require(profile.before == UINT64_C(0x123456789abcdef0) && profile.after == UINT64_C(0x123456789abcdef0), "profiling buffer guards"); // Retain the original mapped buffers across context destruction/recreation. cdsp_vpp_destroy(ctx); } cdsp_vpp_context *a = nullptr, *b = nullptr; require(cdsp_vpp_create(&a, CDSP_VPP_MAX_WORKERS, 1) == 0 && cdsp_vpp_create(&b, 2, 1) == 0, "two live contexts"); uint64_t us; require(cdsp_vpp_scale(a, &src, nullptr, &dst, &us) == 0, "first shared context"); require(cdsp_vpp_scale(b, &src, nullptr, &dst, &us) == 0, "second shared context"); cdsp_vpp_destroy(a); require(cdsp_vpp_scale(b, &src, nullptr, &dst, &us) == 0, "surviving context retains mappings"); auto expected = pixels(dst, dest_size, ((cycles - 1) * 31 + (frames - 1) * 17) & 255); sync(cdsp_vpp_buffer_fd(d), DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ); require(memcmp(dp, expected.data(), dest_size) == 0, "surviving context pixels"); sync(cdsp_vpp_buffer_fd(d), DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ); cdsp_vpp_destroy(b); cdsp_vpp_release(d); cdsp_vpp_release(s); puts("PASS: buffer lifetime, changing pixels, padding, duplicate imports and profiling"); } // Keep enough wide rotation strips alive to exceed the test board's VTCM. // Every context must remain usable when subsequent allocations fall back to DDR. static void check_rotation_contexts() { constexpr size_t source_size = 1920 * 64 * 3 / 2, dest_size = 256 * 3840 * 3 / 2; auto *source = import_test_buffer(source_size), *target = import_test_buffer(dest_size); cdsp_vpp_frame src{source, 1920, 64, 1920, 0, 1920 * 64}; cdsp_vpp_frame dst{target, 128, 3840, 256, 0, 256 * 3840}; cdsp_vpp_context *contexts[5]{}; for (auto &ctx : contexts) require(!cdsp_vpp_create(&ctx, CDSP_VPP_MAX_WORKERS, 1), "rotation context create"); for (int pass = 0; pass < 2; pass++) { int value = 64 + pass * 32; auto input = pixels(src, source_size, value); sync(cdsp_vpp_buffer_fd(source), DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE); memcpy(cdsp_vpp_buffer_map(source), input.data(), source_size); sync(cdsp_vpp_buffer_fd(source), DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE); for (auto *ctx : contexts) { if (!ctx) continue; uint64_t us; require(!cdsp_vpp_transform(ctx, &src, CDSP_VPP_NV12, nullptr, &dst, CDSP_VPP_P010, CDSP_VPP_ROTATE_90, &us), "rotation with multiple live contexts"); sync(cdsp_vpp_buffer_fd(target), DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ); auto *out = static_cast(cdsp_vpp_buffer_map(target)); for (unsigned y = 0; y < dst.height * 3 / 2; y++) for (unsigned x = 0; x < dst.width; x++) { unsigned expected = y < dst.height ? value : (x & 1) ? 255 - value : value / 2; require(out[y * dst.width + x] == expected * 256, "rotation context pixels"); } sync(cdsp_vpp_buffer_fd(target), DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ); } // Releasing one context must not invalidate any surviving workspace. cdsp_vpp_destroy(contexts[0]); contexts[0] = nullptr; } for (auto *ctx : contexts) cdsp_vpp_destroy(ctx); cdsp_vpp_release(target); cdsp_vpp_release(source); puts("PASS: rotation scratch with five live contexts and surviving allocations"); } static void check_transform_boundaries() { constexpr size_t size = 16384; auto *source = import_test_buffer(size), *target = import_test_buffer(size); cdsp_vpp_frame src{source, 288, 8, 600, 16, 16 + 600 * 8}; cdsp_vpp_frame dst{target, 272, 4, 584, 32, 32 + 584 * 6}; cdsp_vpp_crop crop{2, 2, 272, 4}; for (int workers : {0, 1, CDSP_VPP_MAX_WORKERS}) { cdsp_vpp_context *ctx = nullptr; require(!cdsp_vpp_create(&ctx, workers, 1), "conversion context"); for (int sf : {CDSP_VPP_NV12, CDSP_VPP_P010}) for (int df : {CDSP_VPP_NV12, CDSP_VPP_P010}) for (int rotation : {CDSP_VPP_ROTATE_NONE, CDSP_VPP_ROTATE_90, CDSP_VPP_ROTATE_180, CDSP_VPP_ROTATE_270}) { dst = (rotation & 1) ? cdsp_vpp_frame{target, 4, 272, 24, 32, 32 + 24 * 274} : cdsp_vpp_frame{target, 272, 4, 584, 32, 32 + 584 * 6}; std::vector input(size, 0xa5), expected(size, 0xa5); for (unsigned plane = 0; plane < 2; plane++) { unsigned channels = plane ? 2 : 1; for (unsigned y = 0; y < src.height / channels; y++) for (unsigned x = 0; x < src.width; x++) { unsigned value = (x + y * src.width + plane * 71) & (sf ? 1023 : 255); auto *row = input.data() + (plane ? src.uv_offset : src.y_offset) + y * src.stride; if (sf) reinterpret_cast(row)[x] = value << 6; else row[x] = value; } for (unsigned y = 0; y < crop.height / channels; y++) for (unsigned x = 0; x < crop.width; x++) { const auto *row = input.data() + (plane ? src.uv_offset : src.y_offset) + (y + crop.y / channels) * src.stride; unsigned value = sf ? reinterpret_cast(row)[x + crop.x] >> 6 : row[x + crop.x] * 4; unsigned px = x / channels, py = y; if (rotation == CDSP_VPP_ROTATE_90) { px = crop.height / channels - y - 1; py = x / channels; } else if (rotation == CDSP_VPP_ROTATE_180) { px = crop.width / channels - x / channels - 1; py = crop.height / channels - y - 1; } else if (rotation == CDSP_VPP_ROTATE_270) { px = y; py = crop.width / channels - x / channels - 1; } auto *out = expected.data() + (plane ? dst.uv_offset : dst.y_offset) + py * dst.stride; unsigned component = px * channels + x % channels; if (df) reinterpret_cast(out)[component] = value << 6; else out[component] = std::min((value + 2) / 4, 255u); } } sync(cdsp_vpp_buffer_fd(source), DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE); memcpy(cdsp_vpp_buffer_map(source), input.data(), size); sync(cdsp_vpp_buffer_fd(source), DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE); sync(cdsp_vpp_buffer_fd(target), DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE); memset(cdsp_vpp_buffer_map(target), 0xa5, size); sync(cdsp_vpp_buffer_fd(target), DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE); uint64_t us; require(!cdsp_vpp_transform(ctx, &src, sf, &crop, &dst, df, rotation, &us), "conversion, rotation and format switch"); sync(cdsp_vpp_buffer_fd(target), DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ); require(!memcmp(cdsp_vpp_buffer_map(target), expected.data(), size), "conversion values and all padding"); sync(cdsp_vpp_buffer_fd(target), DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ); } uint64_t us; for (int rotation : {-1, CDSP_VPP_ROTATE_270 + 1}) require(cdsp_vpp_transform(ctx, &src, CDSP_VPP_NV12, &crop, &dst, CDSP_VPP_NV12, rotation, &us) != 0, "reject rotation"); require(cdsp_vpp_process(ctx, &src, 2, &crop, &dst, CDSP_VPP_NV12, &us) != 0, "reject format"); auto bad = src; bad.stride = src.width; require(cdsp_vpp_process(ctx, &bad, CDSP_VPP_P010, &crop, &dst, CDSP_VPP_NV12, &us) != 0, "reject short P010 stride"); bad = src; bad.y_offset++; require(cdsp_vpp_process(ctx, &bad, CDSP_VPP_P010, &crop, &dst, CDSP_VPP_NV12, &us) != 0, "reject unaligned P010"); cdsp_vpp_destroy(ctx); } cdsp_vpp_release(target); cdsp_vpp_release(source); puts("PASS: conversion/rotation, format switches, padding and invalid layouts (scalar/1 " "HVX/all HVX)"); } #endif int main(int argc, char **argv) { #ifdef VPP_NO_CDSP gpu = true; #endif for (int i = 1; i < argc; ++i) { if (!std::strcmp(argv[i], "--bench")) benchmark = true; else { require(!std::strcmp(argv[i], "cdsp") || !std::strcmp(argv[i], "vulkan"), "backend argument"); gpu = !std::strcmp(argv[i], "vulkan"); } } #ifdef VPP_NO_CDSP require(gpu, "CDSP tests were disabled at build time"); #endif setenv("IRIS_VAAPI_VPP", gpu ? "vulkan" : "cdsp", 1); std::vector cases = {{1920, 1080, 3840, 2160, false}, {1920, 1080, 1280, 720, false}, {1280, 720, 1920, 1080, false}, {3840, 2160, 1920, 1080, false}, {640, 360, 1280, 720, false}, {640, 360, 640, 360, false}, {640, 384, 320, 180, true}, {16, 2, 32, 4, false}, {32, 4, 16, 2, false}}; for (unsigned src : {VA_FOURCC_NV12, VA_FOURCC_P010}) for (unsigned dst : {VA_FOURCC_NV12, VA_FOURCC_P010}) { if (src == VA_FOURCC_NV12 && dst == VA_FOURCC_NV12) continue; for (Case c : {Case{1920, 1080, 1920, 1080, false}, Case{1920, 1080, 3840, 2160, false}, Case{3840, 2160, 1920, 1080, false}, Case{1920, 1080, 1280, 720, false}, Case{64, 18, 128, 18, false}, Case{128, 36, 128, 18, false}, Case{640, 384, 608, 360, true}, Case{640, 384, 320, 180, true}, Case{80, 18, 160, 36, false}, Case{80, 18, 112, 24, false}, Case{640, 400, 32, 20, false}, Case{16, 2, 16, 2, false}, Case{32, 4, 16, 2, false}}) { c.src_format = src; c.dst_format = dst; cases.push_back(c); } } for (unsigned sf : {VA_FOURCC_NV12, VA_FOURCC_P010}) for (unsigned df : {VA_FOURCC_NV12, VA_FOURCC_P010}) for (unsigned rotation : {VA_ROTATION_90, VA_ROTATION_180, VA_ROTATION_270}) for (Case c : {Case{162, 94, 162, 94, false}, Case{80, 18, 160, 36, false}, Case{320, 180, 160, 90, false}, Case{82, 18, 114, 26, false}, Case{640, 384, 320, 180, true}, Case{2, 2, 2, 2, false}, Case{2, 2, 6, 4, false}}) { c.src_format = sf; c.dst_format = df; c.rotation = rotation; if (rotation & 1) std::swap(c.dw, c.dh); cases.push_back(c); } for (unsigned format : {VA_FOURCC_NV12, VA_FOURCC_P010}) for (unsigned rotation : {VA_ROTATION_90, VA_ROTATION_180, VA_ROTATION_270}) { Case c{1920, 1080, 1920, 1080, false, format, format, rotation}; if (rotation & 1) std::swap(c.dw, c.dh); cases.push_back(c); } cases.push_back( {1080, 1920, 1920, 1080, false, VA_FOURCC_NV12, VA_FOURCC_NV12, VA_ROTATION_90}); // Page-ending buffers expose masked vector accesses even with no active // bytes beyond the boundary. Include the portrait 2x transcode geometry. for (unsigned sf : {VA_FOURCC_NV12, VA_FOURCC_P010}) for (unsigned df : {VA_FOURCC_NV12, VA_FOURCC_P010}) { // Noninteger enlargement: overlapping vector tails, repeated phases, // two-vector gathers near 1:1, and an unchanged horizontal axis. for (Case c : {Case{66, 10, 88, 14, false}, Case{130, 10, 174, 14, false}, Case{288, 162, 384, 216, false}, Case{256, 16, 258, 18, false}, Case{130, 10, 130, 14, false}}) { c.src_format = sf; c.dst_format = df; c.tight = true; cases.push_back(c); } // Four-tap area rows also use the unrolled P010 vertical kernel. cases.push_back({256, 24, 64, 6, false, sf, df}); cases.push_back({720, 1280, 360, 640, false, sf, df}); cases.push_back({64, 128, 96, 192, false, sf, df, VA_ROTATION_NONE, true}); cases.push_back({128, 256, 64, 128, false, sf, df, VA_ROTATION_NONE, true}); cases.push_back({64, 128, 128, 64, false, sf, df, VA_ROTATION_90, true}); cases.push_back({64, 128, 128, 64, false, sf, df, VA_ROTATION_270, true}); } if (benchmark) { cases = {{1920, 1080, 3840, 2160, false}, {3840, 2160, 1920, 1080, false}, {1920, 1080, 1280, 720, false}, {1280, 720, 1920, 1080, false}, {3840, 2160, 3840, 2160, false}, {3840, 2160, 3840, 2160, false, VA_FOURCC_NV12, VA_FOURCC_P010}, {3840, 2160, 3840, 2160, false, VA_FOURCC_P010, VA_FOURCC_NV12}, {1920, 1080, 3840, 2160, false, VA_FOURCC_P010, VA_FOURCC_P010}, {3840, 2160, 2160, 3840, false, VA_FOURCC_NV12, VA_FOURCC_NV12, VA_ROTATION_90}, {3840, 2160, 2160, 3840, false, VA_FOURCC_P010, VA_FOURCC_P010, VA_ROTATION_90}, {1920, 1080, 2160, 3840, false, VA_FOURCC_NV12, VA_FOURCC_P010, VA_ROTATION_270}, {640, 384, 608, 360, true}, {640, 400, 32, 20, false}}; } // FastRPC owns threads and process-global state. Never fork a process // that has already initialized them: each case starts in a fresh child. for (auto c : cases) { pid_t child = fork(); require(child >= 0, "fork test case"); if (!child) { run_case(c); std::exit(0); } int status; require(waitpid(child, &status, 0) == child && status == 0, "test case child"); } #ifndef VPP_NO_CDSP if (!gpu && !benchmark) { check_buffer_lifetime(); check_transform_boundaries(); check_rotation_contexts(); } #endif std::puts("PASS"); } qcom-va-driver-1.1.0/tests/vpp/skel-selection.cpp000066400000000000000000000113401526764642600217620ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "iris_vpp.h" #include "skel.h" #include "vpp_limits.h" #include #include #include #include #include static uint32_t capability; static uint32_t expected_attribute = ARCH_VER; static int query_error, failed_opens, queries; static std::vector opened; static void check(bool ok) { if (!ok) { fprintf(stderr, "FAIL: CDSP selection (attribute=%u, capability=0x%x, failed opens=%d)\n", expected_attribute, capability, failed_opens); std::exit(1); } } extern "C" int remote_handle_control(uint32_t req, void *data, uint32_t size) { check(req == DSPRPC_GET_DSP_INFO && size == sizeof(fastrpc_capability)); auto *request = static_cast(data); check(request->domain == CDSP_DOMAIN_ID && request->attribute_ID == expected_attribute); queries++; request->capability = capability; return query_error; } extern "C" int iris_vpp_open(const char *uri, remote_handle64 *handle) { check(queries == 1); opened.emplace_back(uri); if (opened.size() <= static_cast(failed_opens)) return -ENOENT; *handle = 123; return 0; } static void reset(uint32_t arch, int failures = 0, int error = 0) { capability = arch; failed_opens = failures; query_error = error; queries = 0; opened.clear(); } int main() { const std::string suffix = "?iris_vpp_skel_handle_invoke&_modver=1.0&_dom=cdsp"; for (const char *search_path : {static_cast(nullptr), "", "/tmp/vpp-test"}) { if (search_path) check(setenv("ADSP_LIBRARY_PATH", search_path, 1) == 0); else check(unsetenv("ADSP_LIBRARY_PATH") == 0); for (uint32_t arch : {68u, 69u, 73u, 75u, 79u, 81u}) { uint32_t packed = ((arch / 10) << 4) | (arch % 10); for (uint32_t raw : {arch, packed, 0x100u | packed}) { for (int failures : {0, 1, 2}) { reset(raw, failures); remote_handle64 handle = 0; int e = cdsp_vpp_open_skel(&handle); check(e == (failures == 2 ? -ENOENT : 0)); check(handle == (failures == 2 ? 0 : 123)); check(queries == 1 && opened.size() == (failures ? 2u : 1u)); std::string filename = "vpp_v" + std::to_string(arch) + "_skel.so"; std::string search_uri = "file:///" + filename + suffix; std::string install_uri = "file:///" IRIS_VPP_DSP_DIR "/" + filename + suffix; bool search_first = search_path && *search_path; check(opened[0] == (search_first ? search_uri : install_uri)); if (failures) check(opened[1] == (search_first ? install_uri : search_uri)); } } } } for (uint32_t raw : {0u, 67u, 70u, 74u, 80u, 83u, 0x6fu, 0x74u, 0x83u, UINT32_MAX}) { reset(raw); remote_handle64 handle = 0; check(cdsp_vpp_open_skel(&handle) == -ENOTSUP); check(queries == 1 && opened.empty() && handle == 0); } reset(68, 0, -EIO); remote_handle64 handle = 0; check(cdsp_vpp_open_skel(&handle) == -EIO); check(queries == 1 && opened.empty() && handle == 0); expected_attribute = HVX_SUPPORT_128B; for (uint32_t count : {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 12u, UINT32_MAX}) { for (int requested = 0; requested <= CDSP_VPP_MAX_WORKERS; requested++) { reset(count); int workers = requested; int e = cdsp_vpp_select_workers(&workers); check(e == (!count && requested ? -ENOTSUP : 0)); if (!e) { check(workers <= requested && (uint32_t)workers <= count); check(workers == requested || (uint32_t)workers == count); } else { check(workers == requested); } check(queries == (requested ? 1 : 0) && opened.empty()); } } for (int requested = 0; requested <= CDSP_VPP_MAX_WORKERS; requested++) { reset(4, 0, -EIO); int workers = requested; check(cdsp_vpp_select_workers(&workers) == (requested ? -EIO : 0)); check(workers == requested && queries == (requested ? 1 : 0) && opened.empty()); } reset(8); check(cdsp_vpp_select_workers(nullptr) == -EINVAL); for (int requested : {-1, CDSP_VPP_MAX_WORKERS + 1}) { int workers = requested; check(cdsp_vpp_select_workers(&workers) == -EINVAL && workers == requested); } check(queries == 0 && opened.empty()); puts("PASS: skeleton selection and HVX worker limits, including unavailable/query failure " "paths"); } qcom-va-driver-1.1.0/tests/vpp/tonemap.cpp000066400000000000000000000533661526764642600205220ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Build: c++ -O2 -std=c++17 -rdynamic tests/vpp/tonemap.cpp -o build/vpp-tonemap \ // $(pkg-config --cflags --libs libva libva-drm libdrm vulkan) -ldl // Run with --geometry to check combined processing with the selected VPP backend. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static void require(bool ok, const char *message) { if (!ok) { std::fprintf(stderr, "FAIL: %s\n", message); std::exit(1); } } static void va(VAStatus status) { if (status) { std::fprintf(stderr, "VA: %s\n", vaErrorStr(status)); std::exit(1); } } static void sync(int fd, uint64_t flags) { dma_buf_sync s{flags}; require(ioctl(fd, DMA_BUF_IOCTL_SYNC, &s) == 0, "DMA-BUF CPU sync"); } struct Allocation { VADRMPRIMESurfaceDescriptor desc{}; uint8_t *data; Allocation(unsigned w, unsigned h, unsigned format = VA_FOURCC_NV12, bool tight = false) { auto &obj = desc.objects[0]; auto &l = desc.layers[0]; desc.fourcc = format; l.drm_format = format == VA_FOURCC_P010 ? DRM_FORMAT_P010 : DRM_FORMAT_NV12; desc.width = w; desc.height = h; desc.num_objects = desc.num_layers = 1; l.num_planes = 2; unsigned row_bytes = w * (format == VA_FOURCC_P010 ? 2 : 1); l.pitch[0] = l.pitch[1] = tight ? row_bytes : (row_bytes + 127) & ~127u; // Exercise both padded imports and planes ending at the DMA-BUF boundary. l.offset[0] = tight ? 0 : 128; l.offset[1] = l.offset[0] + l.pitch[0] * (tight ? h : (h + 31) & ~31u); obj.size = (l.offset[1] + l.pitch[1] * ((h + 1) / 2) + 4095) & ~4095u; obj.drm_format_modifier = DRM_FORMAT_MOD_LINEAR; int heap = open("/dev/dma_heap/system", O_RDONLY | O_CLOEXEC); require(heap >= 0, "open system heap"); dma_heap_allocation_data a{}; a.len = obj.size; a.fd_flags = O_RDWR | O_CLOEXEC; require(!ioctl(heap, DMA_HEAP_IOCTL_ALLOC, &a), "allocate DMA-BUF"); close(heap); obj.fd = a.fd; data = static_cast( mmap(nullptr, obj.size, PROT_READ | PROT_WRITE, MAP_SHARED, obj.fd, 0)); require(data != MAP_FAILED, "map allocation"); } ~Allocation() { munmap(data, desc.objects[0].size); close(desc.objects[0].fd); } }; static VASurfaceID import(VADisplay display, VADRMPRIMESurfaceDescriptor &desc) { VASurfaceAttrib attrs[2]{}; attrs[0].type = VASurfaceAttribMemoryType; attrs[0].flags = VA_SURFACE_ATTRIB_SETTABLE; attrs[0].value.type = VAGenericValueTypeInteger; attrs[0].value.value.i = VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2; attrs[1].type = VASurfaceAttribExternalBufferDescriptor; attrs[1].flags = VA_SURFACE_ATTRIB_SETTABLE; attrs[1].value.type = VAGenericValueTypePointer; auto owned = desc; owned.objects[0].fd = dup(desc.objects[0].fd); require(owned.objects[0].fd >= 0, "duplicate descriptor fd"); attrs[1].value.value.p = &owned; VASurfaceID id; va(vaCreateSurfaces( display, desc.fourcc == VA_FOURCC_P010 ? VA_RT_FORMAT_YUV420_10 : VA_RT_FORMAT_YUV420, desc.width, desc.height, &id, 1, attrs, 2)); close(owned.objects[0].fd); // Driver must own its own fd after import. return id; } static unsigned imports = 0; static int expected_input = -1, expected_output = -1; static bool saw_input = false, saw_output = false; static bool same_buffer(int a, int b) { struct stat x{}, y{}; return a >= 0 && b >= 0 && !fstat(a, &x) && !fstat(b, &y) && x.st_dev == y.st_dev && x.st_ino == y.st_ino; } extern "C" VkResult vkAllocateMemory(VkDevice device, const VkMemoryAllocateInfo *info, const VkAllocationCallbacks *callbacks, VkDeviceMemory *memory) { static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "vkAllocateMemory")); for (auto next = static_cast(info->pNext); next; next = next->pNext) { if (next->sType == VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR) { auto fd = reinterpret_cast(next); if (fd->handleType == VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT) { ++imports; saw_input |= same_buffer(fd->fd, expected_input); saw_output |= same_buffer(fd->fd, expected_output); } } } return real(device, info, callbacks, memory); } using RGB = std::array; static double pq(double nits) { double t = std::pow(nits / 10000.0, 0.1593017578125); return std::pow((0.8359375 + 18.8515625 * t) / (1 + 18.6875 * t), 78.84375); } static double linear(double code) { double t = std::pow(std::clamp(code, 0.0, 1.0), 1.0 / 78.84375); return 10000 * std::pow(std::max(t - 0.8359375, 0.0) / (18.8515625 - 18.6875 * t), 1.0 / 0.1593017578125); } static double dot(RGB a, RGB b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; } static std::array reference(unsigned y, unsigned u, unsigned v, double peak) { double yy = (double(y) - 64) / 876, uu = (double(u) - 512) / 896, vv = (double(v) - 512) / 896; double r = yy + 1.4746 * vv, b = yy + 1.8814 * uu; RGB rgb{linear(r), linear((yy - .2627 * r - .0593 * b) / .678), linear(b)}; rgb = {dot(rgb, {1.660491, -.587641, -.072850}), dot(rgb, {-.124550, 1.132900, -.008349}), dot(rgb, {-.018151, -.100579, 1.118730})}; double luma = std::max(dot(rgb, {.2126, .7152, .0722}), 0.0); double negative = std::max(-*std::min_element(rgb.begin(), rgb.end()), 0.0); for (auto &channel : rgb) channel = (channel + negative) * luma / std::max(luma + negative, 1e-20); double maximum = *std::max_element(rgb.begin(), rgb.end()), mapped = maximum; if (peak > 100) { double source_pq = pq(peak), destination = pq(100) / source_pq; double knee = 1.5 * destination - .5, x = std::min(pq(maximum) / source_pq, 1.0); double t = std::clamp((x - knee) / (1 - knee), 0.0, 1.0); // Independent Hermite basis form of the shoulder used by the shader. double shoulder = (2 * t * t * t - 3 * t * t + 1) * knee + (t * t * t - 2 * t * t + t) * (1 - knee) + (-2 * t * t * t + 3 * t * t) * destination; mapped = linear((x < knee ? x : shoulder) * source_pq); } for (auto &channel : rgb) channel = std::pow(std::clamp(channel * mapped / std::max(maximum, 1e-20) / 100, 0.0, 1.0), 1.0 / 2.4); double y709 = dot(rgb, {.2126, .7152, .0722}); return {int(std::nearbyint(64 + 876 * y709)), int(std::nearbyint(512 + (rgb[2] - y709) * 896 / 1.8556)), int(std::nearbyint(512 + (rgb[0] - y709) * 896 / 1.5748))}; } struct Session { int render = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC); VADisplay display; VAConfigID config; VAContextID context; Session() { require(render >= 0, "open render node"); display = vaGetDisplayDRM(render); int major, minor; va(vaInitialize(display, &major, &minor)); va(vaCreateConfig(display, VAProfileNone, VAEntrypointVideoProc, nullptr, 0, &config)); va(vaCreateContext(display, config, 0, 0, 0, nullptr, 0, &context)); VAProcFilterType filters[VAProcFilterCount]; unsigned count = VAProcFilterCount; va(vaQueryVideoProcFilters(display, context, filters, &count)); require(count == 1 && filters[0] == VAProcFilterHighDynamicRangeToneMapping, "HDR filter advertised"); count = 0; require(vaQueryVideoProcFilters(display, context, nullptr, &count) == VA_STATUS_ERROR_MAX_NUM_EXCEEDED && count == 1, "filter capacity check"); VAProcFilterCapHighDynamicRange caps{}; count = 1; va(vaQueryVideoProcFilterCaps(display, context, VAProcFilterHighDynamicRangeToneMapping, &caps, &count)); require(count == 1 && caps.metadata_type == VAProcHighDynamicRangeMetadataHDR10 && caps.caps_flag == VA_TONE_MAPPING_HDR_TO_SDR, "HDR10 to SDR capability"); } ~Session() { va(vaDestroyContext(display, context)); va(vaDestroyConfig(display, config)); va(vaTerminate(display)); close(render); } void submit(VASurfaceID target, VABufferID pipeline) { va(vaBeginPicture(display, context, target)); va(vaRenderPicture(display, context, &pipeline, 1)); va(vaEndPicture(display, context)); } }; static VABufferID pipeline(Session &s, VASurfaceID source, VABufferID *filter, VARectangle *crop = nullptr, unsigned rotation = VA_ROTATION_NONE) { VAProcPipelineParameterBuffer params{}; params.surface = source; params.surface_region = crop; params.filters = filter; params.num_filters = filter ? 1 : 0; params.rotation_state = rotation; if (filter) { params.surface_color_standard = VAProcColorStandardBT2020; params.output_color_standard = VAProcColorStandardBT709; } VABufferID id; va(vaCreateBuffer(s.display, s.context, VAProcPipelineParameterBufferType, sizeof(params), 1, ¶ms, &id)); return id; } static void fill(Allocation &input, uint32_t random = 42) { sync(input.desc.objects[0].fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE); std::memset(input.data, 0xa5, input.desc.objects[0].size); const auto &layout = input.desc.layers[0]; for (unsigned plane = 0; plane < 2; ++plane) for (unsigned y = 0; y < input.desc.height / (plane ? 2 : 1); ++y) for (unsigned x = 0; x < input.desc.width; ++x) { random ^= random << 13; random ^= random >> 17; random ^= random << 5; reinterpret_cast(input.data + layout.offset[plane] + y * layout.pitch[plane])[x] = (random & 1023) << 6; } sync(input.desc.objects[0].fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE); } static void check_pixels(Allocation &input, Allocation &output, VARectangle crop, double peak) { sync(input.desc.objects[0].fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ); sync(output.desc.objects[0].fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ); auto &src = input.desc.layers[0]; auto &dst = output.desc.layers[0]; unsigned max_error = 0; uint64_t total_error = 0; std::vector active(output.desc.objects[0].size); auto compare = [&](unsigned offset, int expected) { unsigned error = std::abs(int(output.data[offset]) - std::clamp(expected, 0, 255)); if (error > 1 && max_error <= 1) std::fprintf(stderr, "pixel offset=%u actual=%u expected=%d\n", offset, output.data[offset], expected); max_error = std::max(max_error, error); total_error += error; active[offset] = true; }; for (unsigned y = 0; y < crop.height; y += 2) for (unsigned x = 0; x < crop.width; x += 2) { auto uv = reinterpret_cast(input.data + src.offset[1] + (y + crop.y) / 2 * src.pitch[1]) + x + crop.x; int sum_u = 0, sum_v = 0; for (unsigned row = 0; row < 2; ++row) for (unsigned column = 0; column < 2; ++column) { auto yy = reinterpret_cast(input.data + src.offset[0] + (y + crop.y + row) * src.pitch[0]); auto expected = reference(yy[x + crop.x + column] >> 6, uv[0] >> 6, uv[1] >> 6, peak); compare(dst.offset[0] + (y + row) * dst.pitch[0] + x + column, (expected[0] + 2) / 4); sum_u += expected[1]; sum_v += expected[2]; } compare(dst.offset[1] + y / 2 * dst.pitch[1] + x, (sum_u + 8) / 16); compare(dst.offset[1] + y / 2 * dst.pitch[1] + x + 1, (sum_v + 8) / 16); } for (unsigned i = 0; i < active.size(); ++i) require(active[i] || output.data[i] == 0xa5, "output padding preserved"); std::printf("%ux%u peak=%.0f max_error=%u MAE=%.6f\n", crop.width, crop.height, peak, max_error, double(total_error) / (crop.width * crop.height * 1.5)); require(max_error <= 1, "FP32 within one NV12 code of double reference"); sync(output.desc.objects[0].fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ); sync(input.desc.objects[0].fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ); } static void run_case(Session &s, unsigned width, unsigned height, unsigned peak, bool cropped = false, bool tight = false, bool geometry = false, unsigned mastering_peak = 0) { VARectangle crop{int16_t(cropped ? 2 : 0), int16_t(cropped ? 2 : 0), uint16_t(width - (cropped ? 2 : 0)), uint16_t(height - (cropped ? 4 : 0))}; Allocation input(width, height, VA_FOURCC_P010, tight); Allocation output(crop.width, crop.height, VA_FOURCC_NV12, tight); fill(input); sync(output.desc.objects[0].fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE); std::memset(output.data, 0xa5, output.desc.objects[0].size); sync(output.desc.objects[0].fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE); VASurfaceID surfaces[] = {import(s.display, input.desc), import(s.display, output.desc)}; VAHdrMetaDataHDR10 metadata{}; metadata.max_content_light_level = peak; metadata.max_display_mastering_luminance = mastering_peak * 10000; VAProcFilterParameterBufferHDRToneMapping hdr{}; hdr.type = VAProcFilterHighDynamicRangeToneMapping; hdr.data.metadata_type = VAProcHighDynamicRangeMetadataHDR10; hdr.data.metadata = &metadata; hdr.data.metadata_size = sizeof(metadata); VABufferID filter; va(vaCreateBuffer(s.display, s.context, VAProcFilterParameterBufferType, sizeof(hdr), 1, &hdr, &filter)); VAProcPipelineCaps caps{}; va(vaQueryVideoProcPipelineCaps(s.display, s.context, &filter, 1, &caps)); require(caps.num_input_pixel_formats == 1 && caps.input_pixel_format[0] == VA_FOURCC_P010 && caps.num_output_pixel_formats == 1 && caps.output_pixel_format[0] == VA_FOURCC_NV12, "HDR formats"); auto params = pipeline(s, surfaces[0], &filter, &crop); if (width == 2) { auto reject = [&](VAStatus expected) { va(vaBeginPicture(s.display, s.context, surfaces[1])); require(vaRenderPicture(s.display, s.context, ¶ms, 1) == expected, "unsupported HDR request rejected"); require(vaEndPicture(s.display, s.context) != VA_STATUS_SUCCESS, "failed render does not submit work"); }; void *mapped; va(vaMapBuffer(s.display, filter, &mapped)); auto *filter_data = static_cast(mapped); filter_data->data.metadata = nullptr; va(vaUnmapBuffer(s.display, filter)); va(vaQueryVideoProcPipelineCaps(s.display, s.context, &filter, 1, &caps)); reject(VA_STATUS_ERROR_INVALID_PARAMETER); va(vaMapBuffer(s.display, filter, &mapped)); std::memcpy(mapped, &hdr, sizeof(hdr)); static_cast(mapped)->data.metadata_size--; va(vaUnmapBuffer(s.display, filter)); reject(VA_STATUS_ERROR_INVALID_PARAMETER); va(vaMapBuffer(s.display, filter, &mapped)); std::memcpy(mapped, &hdr, sizeof(hdr)); va(vaUnmapBuffer(s.display, filter)); va(vaMapBuffer(s.display, params, &mapped)); static_cast(mapped)->input_color_properties.color_range = VA_SOURCE_RANGE_FULL; va(vaUnmapBuffer(s.display, params)); reject(VA_STATUS_ERROR_UNIMPLEMENTED); va(vaMapBuffer(s.display, params, &mapped)); static_cast(mapped)->input_color_properties.color_range = VA_SOURCE_RANGE_UNKNOWN; va(vaUnmapBuffer(s.display, params)); } expected_input = input.desc.objects[0].fd; expected_output = output.desc.objects[0].fd; saw_input = saw_output = false; unsigned before = imports; s.submit(surfaces[1], params); require(imports == before + 2 && saw_input && saw_output, "Vulkan imported original input/output DMA-BUFs"); unsigned source_peak = peak ? peak : mastering_peak ? mastering_peak : 1000; check_pixels(input, output, crop, std::clamp(source_peak, 100u, 10000u)); if (width == 130 && tight) { fill(input, 99); s.submit(surfaces[1], params); check_pixels(input, output, crop, source_peak); } // Metadata may change between frames, and is consumed during RenderPicture. metadata.max_content_light_level = 4000; va(vaBeginPicture(s.display, s.context, surfaces[1])); va(vaRenderPicture(s.display, s.context, ¶ms, 1)); metadata.max_content_light_level = 100; va(vaEndPicture(s.display, s.context)); check_pixels(input, output, crop, 4000); metadata.max_content_light_level = peak; std::vector times; for (unsigned i = 0; i < 110; ++i) { auto start = std::chrono::steady_clock::now(); s.submit(surfaces[1], params); auto end = std::chrono::steady_clock::now(); if (i >= 10) times.push_back(std::chrono::duration(end - start).count()); } require(imports == before + 2, "persistent Vulkan imports"); std::sort(times.begin(), times.end()); std::printf("VAAPI %ux%u median=%.3f ms p95=%.3f ms\n", crop.width, crop.height, times[50], times[95]); VADRMPRIMESurfaceDescriptor exported{}; va(vaExportSurfaceHandle(s.display, surfaces[1], VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_COMPOSED_LAYERS, &exported)); require(exported.num_objects == 1 && same_buffer(exported.objects[0].fd, expected_output), "output DMA-BUF identity"); close(exported.objects[0].fd); if (geometry) { Allocation combined(crop.height / 2, crop.width / 2), separate(crop.height / 2, crop.width / 2); VASurfaceID outputs[] = {import(s.display, combined.desc), import(s.display, separate.desc)}; auto first = pipeline(s, surfaces[0], &filter, &crop, VA_ROTATION_90); auto second = pipeline(s, surfaces[1], nullptr, nullptr, VA_ROTATION_90); for (unsigned frame_peak : {100u, 1000u, 4000u}) { metadata.max_content_light_level = frame_peak; s.submit(surfaces[1], params); s.submit(outputs[0], first); s.submit(outputs[1], second); sync(combined.desc.objects[0].fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ); sync(separate.desc.objects[0].fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ); for (unsigned plane = 0; plane < 2; ++plane) for (unsigned y = 0; y < combined.desc.height / (plane ? 2 : 1); ++y) require(!std::memcmp(combined.data + combined.desc.layers[0].offset[plane] + y * combined.desc.layers[0].pitch[plane], separate.data + separate.desc.layers[0].offset[plane] + y * separate.desc.layers[0].pitch[plane], combined.desc.width), "combined HDR/scale/rotation matches separate operations"); sync(combined.desc.objects[0].fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ); sync(separate.desc.objects[0].fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ); } metadata.max_content_light_level = peak; va(vaDestroyBuffer(s.display, first)); va(vaDestroyBuffer(s.display, second)); va(vaDestroySurfaces(s.display, outputs, 2)); std::puts("combined HDR + scale + rotation PASS"); } va(vaDestroyBuffer(s.display, params)); va(vaDestroyBuffer(s.display, filter)); // Imports must survive destruction of the context that first used them. va(vaDestroyContext(s.display, s.context)); va(vaCreateContext(s.display, s.config, 0, 0, 0, nullptr, 0, &s.context)); va(vaCreateBuffer(s.display, s.context, VAProcFilterParameterBufferType, sizeof(hdr), 1, &hdr, &filter)); params = pipeline(s, surfaces[0], &filter, &crop); unsigned retained = imports; s.submit(surfaces[1], params); require(imports == retained, "imports retained across VPP contexts"); va(vaDestroyBuffer(s.display, params)); va(vaDestroyBuffer(s.display, filter)); va(vaDestroySurfaces(s.display, surfaces, 2)); } int main(int argc, char **argv) { bool geometry = argc == 2 && !std::strcmp(argv[1], "--geometry"); Session session; run_case(session, 2, 2, 100, false, true); run_case(session, 130, 6, 1000, false, true); run_case(session, 132, 10, 0, true); run_case(session, 130, 6, 0, false, false, false, 4000); run_case(session, 256, 128, 10000, false, false, geometry); run_case(session, 1920, 1080, 1000); run_case(session, 3840, 2160, 4000); std::puts("PASS"); }